@@ -0,0 +1,2 @@ |
||
1 |
+https://github.com/cantino/heroku-selectable-procfile.git |
|
2 |
+https://github.com/heroku/heroku-buildpack-ruby.git |
@@ -9,9 +9,9 @@ end |
||
9 | 9 |
|
10 | 10 |
gem 'bundler', '>= 1.5.0' |
11 | 11 |
|
12 |
-gem 'protected_attributes', '~>1.0.7' |
|
12 |
+gem 'protected_attributes', '~>1.0.8' |
|
13 | 13 |
|
14 |
-gem 'rails', '4.1.1' |
|
14 |
+gem 'rails' , '4.1.4' |
|
15 | 15 |
|
16 | 16 |
case RUBY_PLATFORM |
17 | 17 |
when /freebsd/ |
@@ -22,13 +22,13 @@ else |
||
22 | 22 |
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw] |
23 | 23 |
end |
24 | 24 |
|
25 |
-gem 'mysql2', '~> 0.3.15' |
|
25 |
+gem 'mysql2', '~> 0.3.16' |
|
26 | 26 |
gem 'devise', '~> 3.2.4' |
27 |
-gem 'kaminari', '~> 0.15.1' |
|
28 |
-gem 'bootstrap-kaminari-views', '~> 0.0.2' |
|
29 |
-gem 'rufus-scheduler', '~> 3.0.7', require: false |
|
27 |
+gem 'kaminari', '~> 0.16.1' |
|
28 |
+gem 'bootstrap-kaminari-views', '~> 0.0.3' |
|
29 |
+gem 'rufus-scheduler', '~> 3.0.8', require: false |
|
30 | 30 |
gem 'json', '~> 1.8.1' |
31 |
-gem 'jsonpath', '~> 0.5.3' |
|
31 |
+gem 'jsonpath', '~> 0.5.6' |
|
32 | 32 |
gem 'twilio-ruby', '~> 3.11.5' |
33 | 33 |
gem 'ruby-growl', '~> 4.1.0' |
34 | 34 |
gem 'liquid', '~> 2.6.1' |
@@ -64,16 +64,17 @@ gem 'wunderground', '~> 1.2.0' |
||
64 | 64 |
gem 'forecast_io', '~> 2.0.0' |
65 | 65 |
gem 'rturk', '~> 2.12.1' |
66 | 66 |
|
67 |
+gem "google-api-client" |
|
68 |
+ |
|
67 | 69 |
gem 'twitter', '~> 5.8.0' |
68 |
-gem 'twitter-stream', github: 'cantino/twitter-stream', branch: 'master' |
|
70 |
+gem 'cantino-twitter-stream', github: 'cantino/twitter-stream', branch: 'master' |
|
69 | 71 |
gem 'em-http-request', '~> 1.1.2' |
70 | 72 |
gem 'weibo_2', '~> 0.1.4' |
71 | 73 |
gem 'hipchat', '~> 1.2.0' |
72 | 74 |
gem 'xmpp4r', '~> 0.5.6' |
75 |
+gem 'feed-normalizer' |
|
73 | 76 |
gem 'slack-notifier', '~> 0.5.0' |
74 |
- |
|
75 | 77 |
gem 'therubyracer', '~> 0.12.1' |
76 |
- |
|
77 | 78 |
gem 'mqtt' |
78 | 79 |
|
79 | 80 |
gem 'omniauth' |
@@ -84,17 +85,19 @@ gem 'omniauth-github' |
||
84 | 85 |
group :development do |
85 | 86 |
gem 'binding_of_caller' |
86 | 87 |
gem 'better_errors' |
88 |
+ gem 'quiet_assets' |
|
87 | 89 |
end |
88 | 90 |
|
89 | 91 |
group :development, :test do |
92 |
+ gem 'vcr' |
|
90 | 93 |
gem 'dotenv-rails' |
91 | 94 |
gem 'pry' |
92 |
- gem 'rspec-rails' |
|
93 |
- gem 'rspec' |
|
95 |
+ gem 'rspec-rails', '~> 2.14' |
|
96 |
+ gem 'rspec', '~> 2.14' |
|
94 | 97 |
gem 'shoulda-matchers' |
95 | 98 |
gem 'rr' |
96 | 99 |
gem 'delorean' |
97 |
- gem 'webmock', require: false |
|
100 |
+ gem 'webmock', '~> 1.17.4', require: false |
|
98 | 101 |
gem 'coveralls', require: false |
99 | 102 |
end |
100 | 103 |
|
@@ -102,3 +105,17 @@ group :production do |
||
102 | 105 |
gem 'dotenv-deployment' |
103 | 106 |
gem 'rack' |
104 | 107 |
end |
108 |
+ |
|
109 |
+# This hack needs some explanation. When on Heroku, use the pg, unicorn, and rails12factor gems. |
|
110 |
+# When not on Heroku, we still want our Gemfile.lock to include these gems, so we scope them to |
|
111 |
+# an unsupported platform. |
|
112 |
+if ENV['ON_HEROKU'] || ENV['HEROKU_POSTGRESQL_ROSE_URL'] || File.read(File.join(File.dirname(__FILE__), 'Procfile')) =~ /intended for Heroku/ |
|
113 |
+ gem 'pg' |
|
114 |
+ gem 'unicorn' |
|
115 |
+ gem 'rails_12factor' |
|
116 |
+else |
|
117 |
+ gem 'pg', platform: :ruby_18 |
|
118 |
+ gem 'unicorn', platform: :ruby_18 |
|
119 |
+ gem 'rails_12factor', platform: :ruby_18 |
|
120 |
+end |
|
121 |
+ |
@@ -1,9 +1,9 @@ |
||
1 | 1 |
GIT |
2 | 2 |
remote: git://github.com/cantino/twitter-stream.git |
3 |
- revision: fde6bed2b62ca487d49e4a57381bbfca6e33361b |
|
3 |
+ revision: 1c60a1007c50476f23374a8aea796769a088ffe0 |
|
4 | 4 |
branch: master |
5 | 5 |
specs: |
6 |
- twitter-stream (0.1.15) |
|
6 |
+ cantino-twitter-stream (0.1.15) |
|
7 | 7 |
eventmachine (>= 0.12.8) |
8 | 8 |
http_parser.rb (~> 0.6.0) |
9 | 9 |
simple_oauth (~> 0.2.0) |
@@ -12,27 +12,27 @@ GEM |
||
12 | 12 |
remote: https://rubygems.org/ |
13 | 13 |
specs: |
14 | 14 |
ace-rails-ap (2.0.1) |
15 |
- actionmailer (4.1.1) |
|
16 |
- actionpack (= 4.1.1) |
|
17 |
- actionview (= 4.1.1) |
|
15 |
+ actionmailer (4.1.4) |
|
16 |
+ actionpack (= 4.1.4) |
|
17 |
+ actionview (= 4.1.4) |
|
18 | 18 |
mail (~> 2.5.4) |
19 |
- actionpack (4.1.1) |
|
20 |
- actionview (= 4.1.1) |
|
21 |
- activesupport (= 4.1.1) |
|
19 |
+ actionpack (4.1.4) |
|
20 |
+ actionview (= 4.1.4) |
|
21 |
+ activesupport (= 4.1.4) |
|
22 | 22 |
rack (~> 1.5.2) |
23 | 23 |
rack-test (~> 0.6.2) |
24 |
- actionview (4.1.1) |
|
25 |
- activesupport (= 4.1.1) |
|
24 |
+ actionview (4.1.4) |
|
25 |
+ activesupport (= 4.1.4) |
|
26 | 26 |
builder (~> 3.1) |
27 | 27 |
erubis (~> 2.7.0) |
28 |
- activemodel (4.1.1) |
|
29 |
- activesupport (= 4.1.1) |
|
28 |
+ activemodel (4.1.4) |
|
29 |
+ activesupport (= 4.1.4) |
|
30 | 30 |
builder (~> 3.1) |
31 |
- activerecord (4.1.1) |
|
32 |
- activemodel (= 4.1.1) |
|
33 |
- activesupport (= 4.1.1) |
|
31 |
+ activerecord (4.1.4) |
|
32 |
+ activemodel (= 4.1.4) |
|
33 |
+ activesupport (= 4.1.4) |
|
34 | 34 |
arel (~> 5.0.0) |
35 |
- activesupport (4.1.1) |
|
35 |
+ activesupport (4.1.4) |
|
36 | 36 |
i18n (~> 0.6, >= 0.6.9) |
37 | 37 |
json (~> 1.7, >= 1.7.7) |
38 | 38 |
minitest (~> 5.1) |
@@ -40,6 +40,10 @@ GEM |
||
40 | 40 |
tzinfo (~> 1.1) |
41 | 41 |
addressable (2.3.6) |
42 | 42 |
arel (5.0.1.20140414130214) |
43 |
+ autoparse (0.3.3) |
|
44 |
+ addressable (>= 2.3.1) |
|
45 |
+ extlib (>= 0.9.15) |
|
46 |
+ multi_json (>= 1.0.0) |
|
43 | 47 |
bcrypt (3.1.7) |
44 | 48 |
better_errors (1.1.0) |
45 | 49 |
coderay (>= 1.0.0) |
@@ -56,10 +60,10 @@ GEM |
||
56 | 60 |
coffee-rails (4.0.1) |
57 | 61 |
coffee-script (>= 2.2.0) |
58 | 62 |
railties (>= 4.0.0, < 5.0) |
59 |
- coffee-script (2.2.0) |
|
63 |
+ coffee-script (2.3.0) |
|
60 | 64 |
coffee-script-source |
61 | 65 |
execjs |
62 |
- coffee-script-source (1.7.0) |
|
66 |
+ coffee-script-source (1.7.1) |
|
63 | 67 |
cookiejar (0.3.2) |
64 | 68 |
coveralls (0.7.0) |
65 | 69 |
multi_json (~> 1.3) |
@@ -71,7 +75,7 @@ GEM |
||
71 | 75 |
safe_yaml (~> 1.0.0) |
72 | 76 |
daemons (1.1.9) |
73 | 77 |
debug_inspector (0.0.2) |
74 |
- delayed_job (4.0.1) |
|
78 |
+ delayed_job (4.0.2) |
|
75 | 79 |
activesupport (>= 3.0, < 4.2) |
76 | 80 |
delayed_job_active_record (4.0.1) |
77 | 81 |
activerecord (>= 3.0, < 4.2) |
@@ -85,7 +89,7 @@ GEM |
||
85 | 89 |
thread_safe (~> 0.1) |
86 | 90 |
warden (~> 1.2.3) |
87 | 91 |
diff-lcs (1.2.5) |
88 |
- docile (1.1.3) |
|
92 |
+ docile (1.1.5) |
|
89 | 93 |
dotenv (0.11.1) |
90 | 94 |
dotenv-deployment (~> 0.0.2) |
91 | 95 |
dotenv-deployment (0.0.2) |
@@ -103,14 +107,18 @@ GEM |
||
103 | 107 |
erector (0.10.0) |
104 | 108 |
treetop (>= 1.2.3) |
105 | 109 |
erubis (2.7.0) |
106 |
- ethon (0.7.0) |
|
110 |
+ ethon (0.7.1) |
|
107 | 111 |
ffi (>= 1.3.0) |
108 | 112 |
eventmachine (1.0.3) |
109 |
- execjs (2.0.2) |
|
113 |
+ execjs (2.2.1) |
|
114 |
+ extlib (0.9.16) |
|
110 | 115 |
faraday (0.9.0) |
111 | 116 |
multipart-post (>= 1.2, < 3) |
112 | 117 |
faraday_middleware (0.9.1) |
113 | 118 |
faraday (>= 0.7.4, < 0.10) |
119 |
+ feed-normalizer (1.5.2) |
|
120 |
+ hpricot (>= 0.6) |
|
121 |
+ simple-rss (>= 1.1) |
|
114 | 122 |
ffi (1.9.3) |
115 | 123 |
forecast_io (2.0.0) |
116 | 124 |
faraday |
@@ -124,29 +132,43 @@ GEM |
||
124 | 132 |
geokit-rails (2.0.1) |
125 | 133 |
geokit (~> 1.5) |
126 | 134 |
rails (>= 3.0) |
135 |
+ google-api-client (0.7.1) |
|
136 |
+ addressable (>= 2.3.2) |
|
137 |
+ autoparse (>= 0.3.3) |
|
138 |
+ extlib (>= 0.9.15) |
|
139 |
+ faraday (>= 0.9.0) |
|
140 |
+ jwt (>= 0.1.5) |
|
141 |
+ launchy (>= 2.1.1) |
|
142 |
+ multi_json (>= 1.0.0) |
|
143 |
+ retriable (>= 1.4) |
|
144 |
+ signet (>= 0.5.0) |
|
145 |
+ uuidtools (>= 2.1.0) |
|
127 | 146 |
hashie (2.0.5) |
128 | 147 |
hike (1.2.3) |
129 | 148 |
hipchat (1.2.0) |
130 | 149 |
httparty |
131 |
- http (0.5.0) |
|
150 |
+ hpricot (0.8.6) |
|
151 |
+ http (0.5.1) |
|
132 | 152 |
http_parser.rb |
133 | 153 |
http_parser.rb (0.6.0) |
134 | 154 |
httparty (0.13.1) |
135 | 155 |
json (~> 1.8) |
136 | 156 |
multi_xml (>= 0.5.2) |
137 |
- i18n (0.6.9) |
|
138 |
- jquery-rails (3.1.0) |
|
157 |
+ i18n (0.6.11) |
|
158 |
+ jquery-rails (3.1.1) |
|
139 | 159 |
railties (>= 3.0, < 5.0) |
140 | 160 |
thor (>= 0.14, < 2.0) |
141 | 161 |
json (1.8.1) |
142 | 162 |
jsonpath (0.5.6) |
143 | 163 |
multi_json |
144 |
- jwt (0.1.11) |
|
145 |
- multi_json (>= 1.5) |
|
146 |
- kaminari (0.15.1) |
|
164 |
+ jwt (1.0.0) |
|
165 |
+ kaminari (0.16.1) |
|
147 | 166 |
actionpack (>= 3.0.0) |
148 | 167 |
activesupport (>= 3.0.0) |
168 |
+ kgio (2.9.2) |
|
149 | 169 |
kramdown (1.3.3) |
170 |
+ launchy (2.4.2) |
|
171 |
+ addressable (~> 2.3) |
|
150 | 172 |
libv8 (3.16.14.3) |
151 | 173 |
liquid (2.6.1) |
152 | 174 |
macaddr (1.7.1) |
@@ -159,24 +181,24 @@ GEM |
||
159 | 181 |
method_source (0.8.2) |
160 | 182 |
mime-types (1.25.1) |
161 | 183 |
mini_portile (0.6.0) |
162 |
- minitest (5.3.4) |
|
184 |
+ minitest (5.4.0) |
|
163 | 185 |
mqtt (0.2.0) |
164 | 186 |
multi_json (1.10.1) |
165 | 187 |
multi_xml (0.5.5) |
166 | 188 |
multipart-post (2.0.0) |
167 | 189 |
mysql2 (0.3.16) |
168 | 190 |
naught (1.0.0) |
169 |
- nokogiri (1.6.2.1) |
|
191 |
+ nokogiri (1.6.3.1) |
|
170 | 192 |
mini_portile (= 0.6.0) |
171 | 193 |
oauth (0.4.7) |
172 |
- oauth2 (0.9.3) |
|
194 |
+ oauth2 (0.9.4) |
|
173 | 195 |
faraday (>= 0.8, < 0.10) |
174 |
- jwt (~> 0.1.8) |
|
196 |
+ jwt (~> 1.0) |
|
175 | 197 |
multi_json (~> 1.3) |
176 | 198 |
multi_xml (~> 0.5) |
177 | 199 |
rack (~> 1.2) |
178 |
- omniauth (1.2.1) |
|
179 |
- hashie (>= 1.2, < 3) |
|
200 |
+ omniauth (1.2.2) |
|
201 |
+ hashie (>= 1.2, < 4) |
|
180 | 202 |
rack (~> 1.0) |
181 | 203 |
omniauth-37signals (1.0.5) |
182 | 204 |
omniauth (~> 1.0) |
@@ -196,59 +218,75 @@ GEM |
||
196 | 218 |
multi_json (~> 1.3) |
197 | 219 |
omniauth-oauth (~> 1.0) |
198 | 220 |
orm_adapter (0.5.0) |
221 |
+ pg (0.17.1) |
|
199 | 222 |
polyglot (0.3.5) |
200 |
- protected_attributes (1.0.7) |
|
223 |
+ protected_attributes (1.0.8) |
|
201 | 224 |
activemodel (>= 4.0.1, < 5.0) |
202 |
- pry (0.9.12.6) |
|
203 |
- coderay (~> 1.0) |
|
204 |
- method_source (~> 0.8) |
|
225 |
+ pry (0.10.0) |
|
226 |
+ coderay (~> 1.1.0) |
|
227 |
+ method_source (~> 0.8.1) |
|
205 | 228 |
slop (~> 3.4) |
229 |
+ quiet_assets (1.0.3) |
|
230 |
+ railties (>= 3.1, < 5.0) |
|
206 | 231 |
rack (1.5.2) |
207 | 232 |
rack-test (0.6.2) |
208 | 233 |
rack (>= 1.0) |
209 |
- rails (4.1.1) |
|
210 |
- actionmailer (= 4.1.1) |
|
211 |
- actionpack (= 4.1.1) |
|
212 |
- actionview (= 4.1.1) |
|
213 |
- activemodel (= 4.1.1) |
|
214 |
- activerecord (= 4.1.1) |
|
215 |
- activesupport (= 4.1.1) |
|
234 |
+ rails (4.1.4) |
|
235 |
+ actionmailer (= 4.1.4) |
|
236 |
+ actionpack (= 4.1.4) |
|
237 |
+ actionview (= 4.1.4) |
|
238 |
+ activemodel (= 4.1.4) |
|
239 |
+ activerecord (= 4.1.4) |
|
240 |
+ activesupport (= 4.1.4) |
|
216 | 241 |
bundler (>= 1.3.0, < 2.0) |
217 |
- railties (= 4.1.1) |
|
242 |
+ railties (= 4.1.4) |
|
218 | 243 |
sprockets-rails (~> 2.0) |
219 |
- railties (4.1.1) |
|
220 |
- actionpack (= 4.1.1) |
|
221 |
- activesupport (= 4.1.1) |
|
244 |
+ rails_12factor (0.0.2) |
|
245 |
+ rails_serve_static_assets |
|
246 |
+ rails_stdout_logging |
|
247 |
+ rails_serve_static_assets (0.0.2) |
|
248 |
+ rails_stdout_logging (0.0.3) |
|
249 |
+ railties (4.1.4) |
|
250 |
+ actionpack (= 4.1.4) |
|
251 |
+ activesupport (= 4.1.4) |
|
222 | 252 |
rake (>= 0.8.7) |
223 | 253 |
thor (>= 0.18.1, < 2.0) |
254 |
+ raindrops (0.13.0) |
|
224 | 255 |
rake (10.3.2) |
256 |
+ rdoc (4.1.1) |
|
257 |
+ json (~> 1.4) |
|
225 | 258 |
ref (1.0.5) |
226 |
- rest-client (1.6.7) |
|
227 |
- mime-types (>= 1.16) |
|
259 |
+ rest-client (1.6.8) |
|
260 |
+ mime-types (~> 1.16) |
|
261 |
+ rdoc (>= 2.4.2) |
|
262 |
+ retriable (1.4.1) |
|
228 | 263 |
rr (1.1.2) |
229 |
- rspec (2.14.1) |
|
230 |
- rspec-core (~> 2.14.0) |
|
231 |
- rspec-expectations (~> 2.14.0) |
|
232 |
- rspec-mocks (~> 2.14.0) |
|
233 |
- rspec-core (2.14.8) |
|
234 |
- rspec-expectations (2.14.5) |
|
264 |
+ rspec (2.99.0) |
|
265 |
+ rspec-core (~> 2.99.0) |
|
266 |
+ rspec-expectations (~> 2.99.0) |
|
267 |
+ rspec-mocks (~> 2.99.0) |
|
268 |
+ rspec-collection_matchers (1.0.0) |
|
269 |
+ rspec-expectations (>= 2.99.0.beta1) |
|
270 |
+ rspec-core (2.99.1) |
|
271 |
+ rspec-expectations (2.99.2) |
|
235 | 272 |
diff-lcs (>= 1.1.3, < 2.0) |
236 |
- rspec-mocks (2.14.6) |
|
237 |
- rspec-rails (2.14.2) |
|
273 |
+ rspec-mocks (2.99.2) |
|
274 |
+ rspec-rails (2.99.0) |
|
238 | 275 |
actionpack (>= 3.0) |
239 | 276 |
activemodel (>= 3.0) |
240 | 277 |
activesupport (>= 3.0) |
241 | 278 |
railties (>= 3.0) |
242 |
- rspec-core (~> 2.14.0) |
|
243 |
- rspec-expectations (~> 2.14.0) |
|
244 |
- rspec-mocks (~> 2.14.0) |
|
279 |
+ rspec-collection_matchers |
|
280 |
+ rspec-core (~> 2.99.0) |
|
281 |
+ rspec-expectations (~> 2.99.0) |
|
282 |
+ rspec-mocks (~> 2.99.0) |
|
245 | 283 |
rturk (2.12.1) |
246 | 284 |
erector |
247 | 285 |
nokogiri |
248 | 286 |
rest-client |
249 | 287 |
ruby-growl (4.1) |
250 | 288 |
uuid (~> 2.3, >= 2.3.5) |
251 |
- rufus-scheduler (3.0.7) |
|
289 |
+ rufus-scheduler (3.0.8) |
|
252 | 290 |
tzinfo |
253 | 291 |
safe_yaml (1.0.3) |
254 | 292 |
sass (3.2.19) |
@@ -257,18 +295,24 @@ GEM |
||
257 | 295 |
sass (~> 3.2.0) |
258 | 296 |
sprockets (~> 2.8, <= 2.11.0) |
259 | 297 |
sprockets-rails (~> 2.0) |
260 |
- select2-rails (3.5.7) |
|
298 |
+ select2-rails (3.5.9) |
|
261 | 299 |
thor (~> 0.14) |
262 |
- shoulda-matchers (2.6.0) |
|
300 |
+ shoulda-matchers (2.6.2) |
|
263 | 301 |
activesupport (>= 3.0.0) |
302 |
+ signet (0.5.1) |
|
303 |
+ addressable (>= 2.2.3) |
|
304 |
+ faraday (>= 0.9.0.rc5) |
|
305 |
+ jwt (>= 0.1.5) |
|
306 |
+ multi_json (>= 1.0.0) |
|
307 |
+ simple-rss (1.3.1) |
|
264 | 308 |
simple_oauth (0.2.0) |
265 |
- simplecov (0.8.2) |
|
309 |
+ simplecov (0.9.0) |
|
266 | 310 |
docile (~> 1.1.0) |
267 | 311 |
multi_json |
268 | 312 |
simplecov-html (~> 0.8.0) |
269 | 313 |
simplecov-html (0.8.0) |
270 | 314 |
slack-notifier (0.5.0) |
271 |
- slop (3.5.0) |
|
315 |
+ slop (3.6.0) |
|
272 | 316 |
sprockets (2.11.0) |
273 | 317 |
hike (~> 1.2) |
274 | 318 |
multi_json (~> 1.0) |
@@ -287,11 +331,11 @@ GEM |
||
287 | 331 |
thor (0.19.1) |
288 | 332 |
thread_safe (0.3.4) |
289 | 333 |
tilt (1.4.1) |
290 |
- tins (1.1.0) |
|
334 |
+ tins (1.3.0) |
|
291 | 335 |
treetop (1.4.15) |
292 | 336 |
polyglot |
293 | 337 |
polyglot (>= 0.3.1) |
294 |
- twilio-ruby (3.11.5) |
|
338 |
+ twilio-ruby (3.11.6) |
|
295 | 339 |
builder (>= 2.1.2) |
296 | 340 |
jwt (>= 0.1.2) |
297 | 341 |
multi_json (>= 1.3.0) |
@@ -306,15 +350,21 @@ GEM |
||
306 | 350 |
memoizable (~> 0.4.0) |
307 | 351 |
naught (~> 1.0) |
308 | 352 |
simple_oauth (~> 0.2.0) |
309 |
- typhoeus (0.6.8) |
|
310 |
- ethon (>= 0.7.0) |
|
353 |
+ typhoeus (0.6.9) |
|
354 |
+ ethon (>= 0.7.1) |
|
311 | 355 |
tzinfo (1.2.1) |
312 | 356 |
thread_safe (~> 0.1) |
313 |
- uglifier (2.5.0) |
|
357 |
+ uglifier (2.5.3) |
|
314 | 358 |
execjs (>= 0.3.0) |
315 | 359 |
json (>= 1.8.0) |
360 |
+ unicorn (4.8.3) |
|
361 |
+ kgio (~> 2.6) |
|
362 |
+ rack |
|
363 |
+ raindrops (~> 0.7) |
|
316 | 364 |
uuid (2.3.7) |
317 | 365 |
macaddr (~> 1.0) |
366 |
+ uuidtools (2.1.4) |
|
367 |
+ vcr (2.9.2) |
|
318 | 368 |
warden (1.2.3) |
319 | 369 |
rack (>= 1.0) |
320 | 370 |
webmock (1.17.4) |
@@ -338,8 +388,9 @@ DEPENDENCIES |
||
338 | 388 |
ace-rails-ap (~> 2.0.1) |
339 | 389 |
better_errors |
340 | 390 |
binding_of_caller |
341 |
- bootstrap-kaminari-views (~> 0.0.2) |
|
391 |
+ bootstrap-kaminari-views (~> 0.0.3) |
|
342 | 392 |
bundler (>= 1.5.0) |
393 |
+ cantino-twitter-stream! |
|
343 | 394 |
coffee-rails (~> 4.0.0) |
344 | 395 |
coveralls |
345 | 396 |
daemons (~> 1.1.9) |
@@ -352,34 +403,39 @@ DEPENDENCIES |
||
352 | 403 |
em-http-request (~> 1.1.2) |
353 | 404 |
faraday (~> 0.9.0) |
354 | 405 |
faraday_middleware |
406 |
+ feed-normalizer |
|
355 | 407 |
forecast_io (~> 2.0.0) |
356 | 408 |
foreman (~> 0.63.0) |
357 | 409 |
geokit (~> 1.8.4) |
358 | 410 |
geokit-rails (~> 2.0.1) |
411 |
+ google-api-client |
|
359 | 412 |
hipchat (~> 1.2.0) |
360 | 413 |
jquery-rails (~> 3.1.0) |
361 | 414 |
json (~> 1.8.1) |
362 |
- jsonpath (~> 0.5.3) |
|
363 |
- kaminari (~> 0.15.1) |
|
415 |
+ jsonpath (~> 0.5.6) |
|
416 |
+ kaminari (~> 0.16.1) |
|
364 | 417 |
kramdown (~> 1.3.3) |
365 | 418 |
liquid (~> 2.6.1) |
366 | 419 |
mqtt |
367 |
- mysql2 (~> 0.3.15) |
|
420 |
+ mysql2 (~> 0.3.16) |
|
368 | 421 |
nokogiri (~> 1.6.1) |
369 | 422 |
omniauth |
370 | 423 |
omniauth-37signals |
371 | 424 |
omniauth-github |
372 | 425 |
omniauth-twitter |
373 |
- protected_attributes (~> 1.0.7) |
|
426 |
+ pg |
|
427 |
+ protected_attributes (~> 1.0.8) |
|
374 | 428 |
pry |
429 |
+ quiet_assets |
|
375 | 430 |
rack |
376 |
- rails (= 4.1.1) |
|
431 |
+ rails (= 4.1.4) |
|
432 |
+ rails_12factor |
|
377 | 433 |
rr |
378 |
- rspec |
|
379 |
- rspec-rails |
|
434 |
+ rspec (~> 2.14) |
|
435 |
+ rspec-rails (~> 2.14) |
|
380 | 436 |
rturk (~> 2.12.1) |
381 | 437 |
ruby-growl (~> 4.1.0) |
382 |
- rufus-scheduler (~> 3.0.7) |
|
438 |
+ rufus-scheduler (~> 3.0.8) |
|
383 | 439 |
sass-rails (~> 4.0.0) |
384 | 440 |
select2-rails (~> 3.5.4) |
385 | 441 |
shoulda-matchers |
@@ -387,11 +443,12 @@ DEPENDENCIES |
||
387 | 443 |
therubyracer (~> 0.12.1) |
388 | 444 |
twilio-ruby (~> 3.11.5) |
389 | 445 |
twitter (~> 5.8.0) |
390 |
- twitter-stream! |
|
391 | 446 |
typhoeus (~> 0.6.3) |
392 | 447 |
tzinfo-data |
393 | 448 |
uglifier (>= 1.3.0) |
394 |
- webmock |
|
449 |
+ unicorn |
|
450 |
+ vcr |
|
451 |
+ webmock (~> 1.17.4) |
|
395 | 452 |
weibo_2 (~> 0.1.4) |
396 | 453 |
wunderground (~> 1.2.0) |
397 | 454 |
xmpp4r (~> 0.5.6) |
@@ -6,8 +6,8 @@ jobs: bundle exec rails runner bin/threaded.rb |
||
6 | 6 |
# web: bundle exec unicorn -c config/unicorn/production.rb |
7 | 7 |
# jobs: bundle exec rails runner bin/threaded.rb |
8 | 8 |
|
9 |
-# Old version with seperate processes (use this if you have issues with the threaded version) |
|
10 |
-#web: bundle exec rails server |
|
11 |
-#schedule: bundle exec rails runner bin/schedule.rb |
|
12 |
-#twitter: bundle exec rails runner bin/twitter_stream.rb |
|
13 |
-#dj: bundle exec script/delayed_job run |
|
9 |
+# Old version with separate processes (use this if you have issues with the threaded version) |
|
10 |
+# web: bundle exec rails server |
|
11 |
+# schedule: bundle exec rails runner bin/schedule.rb |
|
12 |
+# twitter: bundle exec rails runner bin/twitter_stream.rb |
|
13 |
+# dj: bundle exec script/delayed_job run |
@@ -2,7 +2,7 @@ |
||
2 | 2 |
|
3 | 3 |
## What is Huginn? |
4 | 4 |
|
5 |
-Huginn is a system for building agents that perform automated tasks for you online. They can read the web, watch for events, and take actions on your behalf. Huginn's Agents create and consume events, propagating them along a directed event flow graph. Think of it as Yahoo! Pipes plus IFTTT on your own server. You always know who has your data. You do. |
|
5 |
+Huginn is a system for building agents that perform automated tasks for you online. They can read the web, watch for events, and take actions on your behalf. Huginn's Agents create and consume events, propagating them along a directed graph. Think of it as a hackable Yahoo! Pipes plus IFTTT on your own server. You always know who has your data. You do. |
|
6 | 6 |
|
7 | 7 |
 |
8 | 8 |
|
@@ -10,12 +10,13 @@ Huginn is a system for building agents that perform automated tasks for you onli |
||
10 | 10 |
|
11 | 11 |
* Track the weather and get an email when it's going to rain (or snow) tomorrow ("Don't forget your umbrella!") |
12 | 12 |
* List terms that you care about and receive emails when their occurrence on Twitter changes. (For example, want to know when something interesting has happened in the world of Machine Learning? Huginn will watch the term "machine learning" on Twitter and tell you when there is a large spike.) |
13 |
-* Watch for air travel deals |
|
13 |
+* Watch for air travel or shopping deals |
|
14 | 14 |
* Follow your project names on Twitter and get updates when people mention them |
15 | 15 |
* Scrape websites and receive emails when they change |
16 |
+* Connect to Adioso, HipChat, Basecamp, Growl, FTP, IMAP, Jabber, JIRA, MQTT, nextbus, Pushbullet, Pushover, RSS, Bash, Slack, StubHub, translation APIs, Twilio, Twitter, Wunderground, and Weibo, to name a few. |
|
16 | 17 |
* Compose digest emails about things you care about to be sent at specific times of the day |
17 | 18 |
* Track counts of high frequency events and send an SMS within moments when they spike, such as the term "san francisco emergency" |
18 |
-* Watch public transit |
|
19 |
+* Send and receive WebHooks |
|
19 | 20 |
* Run arbitrary JavaScript Agents on the server |
20 | 21 |
* Track your location over time |
21 | 22 |
* Create Amazon Mechanical Turk workflows as the inputs, or outputs, of agents. ("Once a day, ask 5 people for a funny cat photo; send the results to 5 more people to be rated; send the top-rated photo to 5 people for a funny caption; send to 5 final people to rate for funniest caption; finally, post the best captioned photo on my blog.") |
@@ -26,6 +27,8 @@ Follow [@tectonic](https://twitter.com/tectonic) for updates as Huginn evolves, |
||
26 | 27 |
|
27 | 28 |
Want to help with Huginn? All contributions are encouraged! You could make UI improvements, add new Agents, write documentation and tutorials, or try tackling [issues tagged with #help-wanted](https://github.com/cantino/huginn/issues?direction=desc&labels=help-wanted&page=1&sort=created&state=open). |
28 | 29 |
|
30 |
+Have an awesome an idea but not feeling quite up to contributing yet? Head over to our [Official 'suggest an agent' thread ](https://github.com/cantino/huginn/issues/353) and tell us about your cool idea! |
|
31 |
+ |
|
29 | 32 |
## Examples |
30 | 33 |
|
31 | 34 |
Please checkout the [Huginn Introductory Screencast](http://vimeo.com/61976251)! |
@@ -66,7 +69,7 @@ If you need more detailed instructions, see the [Novice setup guide][novice-setu |
||
66 | 69 |
|
67 | 70 |
## Deployment |
68 | 71 |
|
69 |
-Please see [the Huginn Wiki](https://github.com/cantino/huginn/wiki#deploying-huginn) for detailed deployment strategies for different providers. |
|
72 |
+Huginn can run on Heroku for free! Please see [the Huginn Wiki](https://github.com/cantino/huginn/wiki#deploying-huginn) for detailed deployment strategies for different providers. |
|
70 | 73 |
|
71 | 74 |
### Optional Setup |
72 | 75 |
|
@@ -76,11 +79,7 @@ See [private development instructions](https://github.com/cantino/huginn/wiki/Pr |
||
76 | 79 |
|
77 | 80 |
#### Enable the WeatherAgent |
78 | 81 |
|
79 |
-In order to use the WeatherAgent you need an [API key with Wunderground](http://www.wunderground.com/weather/api/). Signup for one and then change value of `api_key: your-key` in your seeded WeatherAgent. |
|
80 |
- |
|
81 |
-#### Logging your location to the UserLocationAgent |
|
82 |
- |
|
83 |
-You can use [Post Location](https://github.com/cantino/post_location) on your iPhone to post your location to an instance of the UserLocationAgent. Make a new one to see instructions. |
|
82 |
+In order to use the WeatherAgent you need an [API key with Wunderground](http://www.wunderground.com/weather/api/). Signup for one and then change the value of `api_key: your-key` in your seeded WeatherAgent. |
|
84 | 83 |
|
85 | 84 |
#### Enable DelayedJobWeb for handy delayed\_job monitoring and control |
86 | 85 |
|
@@ -102,7 +101,7 @@ Some of us are hanging out there, come and say hello. |
||
102 | 101 |
|
103 | 102 |
## Contribution |
104 | 103 |
|
105 |
-Huginn is a work in progress and is hopefully just getting started. Please get involved! You can [add new Agents](https://github.com/cantino/huginn/wiki/Creating-a-new-agent), expand the [Wiki](https://github.com/cantino/huginn/wiki), or help us simplify and strengthen the Agent API or core application. |
|
104 |
+Huginn is a work in progress and is just getting started. Please get involved! You can [add new Agents](https://github.com/cantino/huginn/wiki/Creating-a-new-agent), expand the [Wiki](https://github.com/cantino/huginn/wiki), or help us simplify and strengthen the Agent API or core application. |
|
106 | 105 |
|
107 | 106 |
Please fork, add specs, and send pull requests! |
108 | 107 |
|
@@ -25,7 +25,10 @@ hideSchedule = -> |
||
25 | 25 |
$(".schedule-region select").hide() |
26 | 26 |
$(".schedule-region .cannot-be-scheduled").show() |
27 | 27 |
|
28 |
-showSchedule = -> |
|
28 |
+showSchedule = (defaultSchedule = null) -> |
|
29 |
+ $(".schedule-region select").show() |
|
30 |
+ if defaultSchedule? |
|
31 |
+ $(".schedule-region select").val(defaultSchedule).change() |
|
29 | 32 |
$(".schedule-region select").show() |
30 | 33 |
$(".schedule-region .cannot-be-scheduled").hide() |
31 | 34 |
|
@@ -65,7 +68,7 @@ $(document).ready -> |
||
65 | 68 |
setTimeout((-> $(".flash").slideUp(-> $(".flash").remove())), 5000) |
66 | 69 |
|
67 | 70 |
# Help popovers |
68 |
- $('.hover-help').popover(trigger: 'hover') |
|
71 |
+ $('.hover-help').popover(trigger: 'hover', html: true) |
|
69 | 72 |
|
70 | 73 |
# Agent Navigation |
71 | 74 |
$agentNavigate = $('#agent-navigate') |
@@ -145,7 +148,7 @@ $(document).ready -> |
||
145 | 148 |
$(".event-descriptions").html("").hide() |
146 | 149 |
$.getJSON "/agents/type_details", { type: $(@).val() }, (json) => |
147 | 150 |
if json.can_be_scheduled |
148 |
- showSchedule() |
|
151 |
+ showSchedule(json.default_schedule) |
|
149 | 152 |
else |
150 | 153 |
hideSchedule() |
151 | 154 |
|
@@ -156,6 +156,12 @@ span.not-applicable:after { |
||
156 | 156 |
top: 2px; |
157 | 157 |
} |
158 | 158 |
|
159 |
+.popover { |
|
160 |
+ dd { |
|
161 |
+ margin-left: 1em; |
|
162 |
+ } |
|
163 |
+} |
|
164 |
+ |
|
159 | 165 |
h2 .scenario, a span.label.scenario { |
160 | 166 |
position: relative; |
161 | 167 |
top: -2px; |
@@ -9,6 +9,27 @@ module EmailConcern |
||
9 | 9 |
|
10 | 10 |
def validate_email_options |
11 | 11 |
errors.add(:base, "subject and expected_receive_period_in_days are required") unless options['subject'].present? && options['expected_receive_period_in_days'].present? |
12 |
+ |
|
13 |
+ if options['recipients'].present? |
|
14 |
+ emails = options['recipients'] |
|
15 |
+ emails = [emails] if emails.is_a?(String) |
|
16 |
+ unless emails.all? { |email| email =~ Devise.email_regexp } |
|
17 |
+ errors.add(:base, "'when provided, 'recipients' should be an email address or an array of email addresses") |
|
18 |
+ end |
|
19 |
+ end |
|
20 |
+ end |
|
21 |
+ |
|
22 |
+ def recipients(payload = {}) |
|
23 |
+ emails = interpolated(payload)['recipients'] |
|
24 |
+ if emails.present? |
|
25 |
+ if emails.is_a?(String) |
|
26 |
+ [emails] |
|
27 |
+ else |
|
28 |
+ emails |
|
29 |
+ end |
|
30 |
+ else |
|
31 |
+ [user.email] |
|
32 |
+ end |
|
12 | 33 |
end |
13 | 34 |
|
14 | 35 |
def working? |
@@ -0,0 +1,17 @@ |
||
1 |
+module LiquidDroppable |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ |
|
4 |
+ class Drop < Liquid::Drop |
|
5 |
+ def initialize(object) |
|
6 |
+ @object = object |
|
7 |
+ end |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ included do |
|
11 |
+ const_set :Drop, Kernel.const_set("#{name}Drop", Class.new(Drop)) |
|
12 |
+ end |
|
13 |
+ |
|
14 |
+ def to_liquid(*args) |
|
15 |
+ self.class::Drop.new(self, *args) |
|
16 |
+ end |
|
17 |
+end |
@@ -1,28 +1,28 @@ |
||
1 | 1 |
module LiquidInterpolatable |
2 | 2 |
extend ActiveSupport::Concern |
3 | 3 |
|
4 |
- def interpolate_options(options, payload = {}) |
|
4 |
+ def interpolate_options(options, event = {}) |
|
5 | 5 |
case options |
6 | 6 |
when String |
7 |
- interpolate_string(options, payload) |
|
7 |
+ interpolate_string(options, event) |
|
8 | 8 |
when ActiveSupport::HashWithIndifferentAccess, Hash |
9 |
- options.inject(ActiveSupport::HashWithIndifferentAccess.new) { |memo, (key, value)| memo[key] = interpolate_options(value, payload); memo } |
|
9 |
+ options.inject(ActiveSupport::HashWithIndifferentAccess.new) { |memo, (key, value)| memo[key] = interpolate_options(value, event); memo } |
|
10 | 10 |
when Array |
11 |
- options.map { |value| interpolate_options(value, payload) } |
|
11 |
+ options.map { |value| interpolate_options(value, event) } |
|
12 | 12 |
else |
13 | 13 |
options |
14 | 14 |
end |
15 | 15 |
end |
16 | 16 |
|
17 |
- def interpolated(payload = {}) |
|
18 |
- key = [options, payload] |
|
17 |
+ def interpolated(event = {}) |
|
18 |
+ key = [options, event] |
|
19 | 19 |
@interpolated_cache ||= {} |
20 |
- @interpolated_cache[key] ||= interpolate_options(options, payload) |
|
20 |
+ @interpolated_cache[key] ||= interpolate_options(options, event) |
|
21 | 21 |
@interpolated_cache[key] |
22 | 22 |
end |
23 | 23 |
|
24 |
- def interpolate_string(string, payload) |
|
25 |
- Liquid::Template.parse(string).render!(payload, registers: {agent: self}) |
|
24 |
+ def interpolate_string(string, event) |
|
25 |
+ Liquid::Template.parse(string).render!(event.to_liquid, registers: {agent: self}) |
|
26 | 26 |
end |
27 | 27 |
|
28 | 28 |
require 'uri' |
@@ -0,0 +1,61 @@ |
||
1 |
+module WebRequestConcern |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ |
|
4 |
+ def validate_web_request_options! |
|
5 |
+ if options['user_agent'].present? |
|
6 |
+ errors.add(:base, "user_agent must be a string") unless options['user_agent'].is_a?(String) |
|
7 |
+ end |
|
8 |
+ |
|
9 |
+ unless headers(options['headers']).is_a?(Hash) |
|
10 |
+ errors.add(:base, "if provided, headers must be a hash") |
|
11 |
+ end |
|
12 |
+ |
|
13 |
+ begin |
|
14 |
+ basic_auth_credentials(options['basic_auth']) |
|
15 |
+ rescue ArgumentError => e |
|
16 |
+ errors.add(:base, e.message) |
|
17 |
+ end |
|
18 |
+ end |
|
19 |
+ |
|
20 |
+ def faraday |
|
21 |
+ @faraday ||= Faraday.new { |builder| |
|
22 |
+ builder.headers = headers if headers.length > 0 |
|
23 |
+ |
|
24 |
+ if (user_agent = interpolated['user_agent']).present? |
|
25 |
+ builder.headers[:user_agent] = user_agent |
|
26 |
+ end |
|
27 |
+ |
|
28 |
+ builder.use FaradayMiddleware::FollowRedirects |
|
29 |
+ builder.request :url_encoded |
|
30 |
+ if userinfo = basic_auth_credentials |
|
31 |
+ builder.request :basic_auth, *userinfo |
|
32 |
+ end |
|
33 |
+ |
|
34 |
+ case backend = faraday_backend |
|
35 |
+ when :typhoeus |
|
36 |
+ require 'typhoeus/adapters/faraday' |
|
37 |
+ end |
|
38 |
+ builder.adapter backend |
|
39 |
+ } |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ def headers(value = interpolated['headers']) |
|
43 |
+ value.presence || {} |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ def basic_auth_credentials(value = interpolated['basic_auth']) |
|
47 |
+ case value |
|
48 |
+ when nil, '' |
|
49 |
+ return nil |
|
50 |
+ when Array |
|
51 |
+ return value if value.size == 2 |
|
52 |
+ when /:/ |
|
53 |
+ return value.split(/:/, 2) |
|
54 |
+ end |
|
55 |
+ raise ArgumentError.new("bad value for basic_auth: #{value.inspect}") |
|
56 |
+ end |
|
57 |
+ |
|
58 |
+ def faraday_backend |
|
59 |
+ ENV.fetch('FARADAY_HTTP_BACKEND', 'typhoeus').to_sym |
|
60 |
+ end |
|
61 |
+end |
@@ -31,14 +31,14 @@ class AgentsController < ApplicationController |
||
31 | 31 |
end |
32 | 32 |
|
33 | 33 |
def type_details |
34 |
- @agent = Agent.build_for_type(params[:type], current_user, {}) |
|
34 |
+ agent = Agent.build_for_type(params[:type], current_user, {}) |
|
35 | 35 |
render :json => { |
36 |
- :can_be_scheduled => @agent.can_be_scheduled?, |
|
37 |
- :can_receive_events => @agent.can_receive_events?, |
|
38 |
- :can_create_events => @agent.can_create_events?, |
|
39 |
- :options => @agent.default_options, |
|
40 |
- :description_html => @agent.html_description, |
|
41 |
- :form => render_to_string(partial: 'form') |
|
36 |
+ :can_be_scheduled => agent.can_be_scheduled?, |
|
37 |
+ :default_schedule => agent.default_schedule, |
|
38 |
+ :can_receive_events => agent.can_receive_events?, |
|
39 |
+ :can_create_events => agent.can_create_events?, |
|
40 |
+ :options => agent.default_options, |
|
41 |
+ :description_html => agent.html_description |
|
42 | 42 |
} |
43 | 43 |
end |
44 | 44 |
|
@@ -7,6 +7,14 @@ module ApplicationHelper |
||
7 | 7 |
HTML |
8 | 8 |
end |
9 | 9 |
|
10 |
+ def yes_no(bool) |
|
11 |
+ if bool |
|
12 |
+ '<span class="label label-info">Yes</span>'.html_safe |
|
13 |
+ else |
|
14 |
+ '<span class="label label-default">No</span>'.html_safe |
|
15 |
+ end |
|
16 |
+ end |
|
17 |
+ |
|
10 | 18 |
def working(agent) |
11 | 19 |
if agent.disabled? |
12 | 20 |
link_to 'Disabled', agent_path(agent), :class => 'label label-warning' |
@@ -14,32 +14,150 @@ module DotHelper |
||
14 | 14 |
end |
15 | 15 |
end |
16 | 16 |
|
17 |
- private |
|
17 |
+ class DotDrawer |
|
18 |
+ def initialize(vars = {}) |
|
19 |
+ @dot = '' |
|
20 |
+ vars.each { |name, value| |
|
21 |
+ # Import variables as methods |
|
22 |
+ define_singleton_method(name) { value } |
|
23 |
+ } |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ def to_s |
|
27 |
+ @dot |
|
28 |
+ end |
|
29 |
+ |
|
30 |
+ def self.draw(*args, &block) |
|
31 |
+ drawer = new(*args) |
|
32 |
+ drawer.instance_exec(&block) |
|
33 |
+ drawer.to_s |
|
34 |
+ end |
|
35 |
+ |
|
36 |
+ def raw(string) |
|
37 |
+ @dot << string |
|
38 |
+ end |
|
39 |
+ |
|
40 |
+ def escape(string) |
|
41 |
+ # Backslash escaping seems to work for the backslash itself, |
|
42 |
+ # though it's not documented in the DOT language docs. |
|
43 |
+ string.gsub(/[\\"\n]/, |
|
44 |
+ "\\" => "\\\\", |
|
45 |
+ "\"" => "\\\"", |
|
46 |
+ "\n" => "\\n") |
|
47 |
+ end |
|
48 |
+ |
|
49 |
+ def id(value) |
|
50 |
+ case string = value.to_s |
|
51 |
+ when /\A(?!\d)\w+\z/, /\A(?:\.\d+|\d+(?:\.\d*)?)\z/ |
|
52 |
+ raw string |
|
53 |
+ else |
|
54 |
+ raw '"' |
|
55 |
+ raw escape(string) |
|
56 |
+ raw '"' |
|
57 |
+ end |
|
58 |
+ end |
|
59 |
+ |
|
60 |
+ def attr_list(attrs = nil) |
|
61 |
+ return if attrs.nil? |
|
62 |
+ attrs = attrs.select { |key, value| value.present? } |
|
63 |
+ return if attrs.empty? |
|
64 |
+ raw '[' |
|
65 |
+ attrs.each_with_index { |(key, value), i| |
|
66 |
+ raw ',' if i > 0 |
|
67 |
+ id key |
|
68 |
+ raw '=' |
|
69 |
+ id value |
|
70 |
+ } |
|
71 |
+ raw ']' |
|
72 |
+ end |
|
18 | 73 |
|
19 |
- def dot_id(string) |
|
20 |
- # Backslash escaping seems to work for the backslash itself, |
|
21 |
- # despite the DOT language document. |
|
22 |
- '"%s"' % string.gsub(/\\/, "\\\\\\\\").gsub(/"/, "\\\\\"") |
|
74 |
+ def node(id, attrs = nil) |
|
75 |
+ id id |
|
76 |
+ attr_list attrs |
|
77 |
+ raw ';' |
|
78 |
+ end |
|
79 |
+ |
|
80 |
+ def edge(from, to, attrs = nil, op = '->') |
|
81 |
+ id from |
|
82 |
+ raw op |
|
83 |
+ id to |
|
84 |
+ attr_list attrs |
|
85 |
+ raw ';' |
|
86 |
+ end |
|
87 |
+ |
|
88 |
+ def statement(ids, attrs = nil) |
|
89 |
+ Array(ids).each_with_index { |id, i| |
|
90 |
+ raw ' ' if i > 0 |
|
91 |
+ id id |
|
92 |
+ } |
|
93 |
+ attr_list attrs |
|
94 |
+ raw ';' |
|
95 |
+ end |
|
96 |
+ |
|
97 |
+ def block(title, &block) |
|
98 |
+ raw title |
|
99 |
+ raw '{' |
|
100 |
+ block.call |
|
101 |
+ raw '}' |
|
102 |
+ end |
|
23 | 103 |
end |
24 | 104 |
|
25 |
- def disabled_label(agent) |
|
26 |
- agent.disabled? ? dot_id(agent.name + " (Disabled)") : dot_id(agent.name) |
|
105 |
+ private |
|
106 |
+ |
|
107 |
+ def draw(vars = {}, &block) |
|
108 |
+ DotDrawer.draw(vars, &block) |
|
27 | 109 |
end |
28 | 110 |
|
29 | 111 |
def agents_dot(agents, rich = false) |
30 |
- "digraph foo {".tap { |dot| |
|
31 |
- agents.each.with_index do |agent, index| |
|
32 |
- if rich |
|
33 |
- dot << '%s[URL=%s];' % [disabled_label(agent), dot_id(agent_path(agent.id))] |
|
34 |
- else |
|
35 |
- dot << '%s;' % disabled_label(agent) |
|
36 |
- end |
|
37 |
- agent.receivers.each do |receiver| |
|
38 |
- next unless agents.include?(receiver) |
|
39 |
- dot << "%s->%s;" % [disabled_label(agent), disabled_label(receiver)] |
|
40 |
- end |
|
112 |
+ draw(agents: agents, |
|
113 |
+ agent_id: ->agent { 'a%d' % agent.id }, |
|
114 |
+ agent_label: ->agent { |
|
115 |
+ if agent.disabled? |
|
116 |
+ '%s (Disabled)' % agent.name |
|
117 |
+ else |
|
118 |
+ agent.name |
|
119 |
+ end.gsub(/(.{20}\S*)\s+/) { |
|
120 |
+ # Fold after every 20+ characters |
|
121 |
+ $1 + "\n" |
|
122 |
+ } |
|
123 |
+ }, |
|
124 |
+ agent_url: ->agent { agent_path(agent.id) }, |
|
125 |
+ rich: rich) { |
|
126 |
+ @disabled = '#999999' |
|
127 |
+ |
|
128 |
+ def agent_node(agent) |
|
129 |
+ node(agent_id[agent], |
|
130 |
+ label: agent_label[agent], |
|
131 |
+ URL: (agent_url[agent] if rich), |
|
132 |
+ style: ('rounded,dashed' if agent.disabled?), |
|
133 |
+ color: (@disabled if agent.disabled?), |
|
134 |
+ fontcolor: (@disabled if agent.disabled?)) |
|
135 |
+ end |
|
136 |
+ |
|
137 |
+ def agent_edge(agent, receiver) |
|
138 |
+ edge(agent_id[agent], |
|
139 |
+ agent_id[receiver], |
|
140 |
+ style: ('dashed' unless receiver.propagate_immediately), |
|
141 |
+ color: (@disabled if agent.disabled? || receiver.disabled?)) |
|
41 | 142 |
end |
42 |
- dot << "}" |
|
143 |
+ |
|
144 |
+ block('digraph foo') { |
|
145 |
+ # statement 'graph', rankdir: 'LR' |
|
146 |
+ statement 'node', |
|
147 |
+ shape: 'box', |
|
148 |
+ style: 'rounded', |
|
149 |
+ target: '_blank', |
|
150 |
+ fontsize: 10, |
|
151 |
+ fontname: ('Helvetica' if rich) |
|
152 |
+ |
|
153 |
+ agents.each.with_index { |agent, index| |
|
154 |
+ agent_node(agent) |
|
155 |
+ |
|
156 |
+ agent.receivers.each { |receiver| |
|
157 |
+ agent_edge(agent, receiver) if agents.include?(receiver) |
|
158 |
+ } |
|
159 |
+ } |
|
160 |
+ } |
|
43 | 161 |
} |
44 | 162 |
end |
45 | 163 |
end |
@@ -14,6 +14,7 @@ class Agent < ActiveRecord::Base |
||
14 | 14 |
include WorkingHelpers |
15 | 15 |
include LiquidInterpolatable |
16 | 16 |
include HasGuid |
17 |
+ include LiquidDroppable |
|
17 | 18 |
|
18 | 19 |
markdown_class_attributes :description, :event_description |
19 | 20 |
|
@@ -67,6 +68,10 @@ class Agent < ActiveRecord::Base |
||
67 | 68 |
where(:type => type) |
68 | 69 |
} |
69 | 70 |
|
71 |
+ def short_type |
|
72 |
+ type.demodulize |
|
73 |
+ end |
|
74 |
+ |
|
70 | 75 |
def check |
71 | 76 |
# Implement me in your subclass of Agent. |
72 | 77 |
end |
@@ -226,6 +231,19 @@ class Agent < ActiveRecord::Base |
||
226 | 231 |
# Implement me in your subclass to test for valid options. |
227 | 232 |
end |
228 | 233 |
|
234 |
+ # Utility Methods |
|
235 |
+ |
|
236 |
+ def boolify(option_value) |
|
237 |
+ case option_value |
|
238 |
+ when true, 'true' |
|
239 |
+ true |
|
240 |
+ when false, 'false' |
|
241 |
+ false |
|
242 |
+ else |
|
243 |
+ nil |
|
244 |
+ end |
|
245 |
+ end |
|
246 |
+ |
|
229 | 247 |
# Class Methods |
230 | 248 |
|
231 | 249 |
class << self |
@@ -366,3 +384,36 @@ class Agent < ActiveRecord::Base |
||
366 | 384 |
handle_asynchronously :async_check |
367 | 385 |
end |
368 | 386 |
end |
387 |
+ |
|
388 |
+class AgentDrop |
|
389 |
+ def type |
|
390 |
+ @object.short_type |
|
391 |
+ end |
|
392 |
+ |
|
393 |
+ METHODS = [ |
|
394 |
+ :name, |
|
395 |
+ :type, |
|
396 |
+ :options, |
|
397 |
+ :memory, |
|
398 |
+ :sources, |
|
399 |
+ :receivers, |
|
400 |
+ :schedule, |
|
401 |
+ :disabled, |
|
402 |
+ :keep_events_for, |
|
403 |
+ :propagate_immediately, |
|
404 |
+ ] |
|
405 |
+ |
|
406 |
+ METHODS.each { |attr| |
|
407 |
+ define_method(attr) { |
|
408 |
+ @object.__send__(attr) |
|
409 |
+ } unless method_defined?(attr) |
|
410 |
+ } |
|
411 |
+ |
|
412 |
+ def each(&block) |
|
413 |
+ return to_enum(__method__) unless block |
|
414 |
+ |
|
415 |
+ METHODS.each { |attr| |
|
416 |
+ yield [attr, __sent__(attr)] |
|
417 |
+ } |
|
418 |
+ end |
|
419 |
+end |
@@ -83,7 +83,7 @@ module Agents |
||
83 | 83 |
def receive_web_request(params, method, format) |
84 | 84 |
if interpolated['secrets'].include?(params['secret']) |
85 | 85 |
items = received_events.order('id desc').limit(events_to_show).map do |event| |
86 |
- interpolated = interpolate_options(options['template']['item'], event.payload) |
|
86 |
+ interpolated = interpolate_options(options['template']['item'], event) |
|
87 | 87 |
interpolated['guid'] = event.id |
88 | 88 |
interpolated['pubDate'] = event.created_at.rfc2822.to_s |
89 | 89 |
interpolated |
@@ -7,9 +7,12 @@ module Agents |
||
7 | 7 |
|
8 | 8 |
description <<-MD |
9 | 9 |
The EmailAgent sends any events it receives via email immediately. |
10 |
- The email will be sent to your account's address and will have a `subject` and an optional `headline` before |
|
11 |
- listing the Events. If the Events' payloads contain a `:message`, that will be highlighted, otherwise everything in |
|
12 |
- their payloads will be shown. |
|
10 |
+ |
|
11 |
+ The email will have a `subject` and an optional `headline` before listing the Events. If the Events' payloads |
|
12 |
+ contain a `:message`, that will be highlighted, otherwise everything in their payloads will be shown. |
|
13 |
+ |
|
14 |
+ You can specify one or more `recipients` for the email, or skip the option in order to send the email to your |
|
15 |
+ account's default email address. |
|
13 | 16 |
|
14 | 17 |
Set `expected_receive_period_in_days` to the maximum amount of time that you'd expect to pass between Events being received by this Agent. |
15 | 18 |
MD |
@@ -25,7 +28,9 @@ module Agents |
||
25 | 28 |
def receive(incoming_events) |
26 | 29 |
incoming_events.each do |event| |
27 | 30 |
log "Sending digest mail to #{user.email} with event #{event.id}" |
28 |
- SystemMailer.delay.send_message(:to => user.email, :subject => interpolated(event.payload)['subject'], :headline => interpolated(event.payload)['headline'], :groups => [present(event.payload)]) |
|
31 |
+ recipients(event.payload).each do |recipient| |
|
32 |
+ SystemMailer.delay.send_message(:to => recipient, :subject => interpolated(event)['subject'], :headline => interpolated(event)['headline'], :groups => [present(event.payload)]) |
|
33 |
+ end |
|
29 | 34 |
end |
30 | 35 |
end |
31 | 36 |
end |
@@ -7,11 +7,15 @@ module Agents |
||
7 | 7 |
cannot_create_events! |
8 | 8 |
|
9 | 9 |
description <<-MD |
10 |
- The EmailDigestAgent collects any Events sent to it and sends them all via email when run. |
|
11 |
- The email will be sent to your account's address and will have a `subject` and an optional `headline` before |
|
12 |
- listing the Events. If the Events' payloads contain a `message`, that will be highlighted, otherwise everything in |
|
10 |
+ The EmailDigestAgent collects any Events sent to it and sends them all via email when scheduled. |
|
11 |
+ |
|
12 |
+ By default, the will have a `subject` and an optional `headline` before listing the Events. If the Events' |
|
13 |
+ payloads contain a `message`, that will be highlighted, otherwise everything in |
|
13 | 14 |
their payloads will be shown. |
14 | 15 |
|
16 |
+ You can specify one or more `recipients` for the email, or skip the option in order to send the email to your |
|
17 |
+ account's default email address. |
|
18 |
+ |
|
15 | 19 |
Set `expected_receive_period_in_days` to the maximum amount of time that you'd expect to pass between Events being received by this Agent. |
16 | 20 |
MD |
17 | 21 |
|
@@ -37,7 +41,9 @@ module Agents |
||
37 | 41 |
ids = self.memory['events'].join(",") |
38 | 42 |
groups = self.memory['queue'].map { |payload| present(payload) } |
39 | 43 |
log "Sending digest mail to #{user.email} with events [#{ids}]" |
40 |
- SystemMailer.delay.send_message(:to => user.email, :subject => interpolated['subject'], :headline => interpolated['headline'], :groups => groups) |
|
44 |
+ recipients.each do |recipient| |
|
45 |
+ SystemMailer.delay.send_message(:to => recipient, :subject => interpolated['subject'], :headline => interpolated['headline'], :groups => groups) |
|
46 |
+ end |
|
41 | 47 |
self.memory['queue'] = [] |
42 | 48 |
self.memory['events'] = [] |
43 | 49 |
end |
@@ -28,6 +28,8 @@ module Agents |
||
28 | 28 |
"subject": "{{data}}" |
29 | 29 |
} |
30 | 30 |
|
31 |
+ The upstream agent of each received event is accessible via the key `agent`, which has the following attributes: #{''.tap { |s| s << AgentDrop.instance_methods(false).map { |m| "`#{m}`" }.join(', ') }}. |
|
32 |
+ |
|
31 | 33 |
Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating. |
32 | 34 |
|
33 | 35 |
Events generated by this possible Event Formatting Agent will look like: |
@@ -60,13 +62,13 @@ module Agents |
||
60 | 62 |
So you can use it in `instructions` like this: |
61 | 63 |
|
62 | 64 |
"instructions": { |
63 |
- "message": "Today's conditions look like <$.conditions> with a high temperature of {{high.celsius}} degrees Celsius according to the forecast at {{pretty_date.time}}.", |
|
65 |
+ "message": "Today's conditions look like {{conditions}} with a high temperature of {{high.celsius}} degrees Celsius according to the forecast at {{pretty_date.time}}.", |
|
64 | 66 |
"subject": "{{data}}" |
65 | 67 |
} |
66 | 68 |
|
67 | 69 |
If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`. |
68 | 70 |
|
69 |
- By default, the output event will have `agent` and `created_at` fields added as well, reflecting the original Agent type and Event creation time. You can skip these outputs by setting `skip_agent` and `skip_created_at` to `true`. |
|
71 |
+ By default, the output event will have a `created_at` field added as well, reflecting the original Event creation time. You can skip this output by setting `skip_created_at` to `true`. |
|
70 | 72 |
|
71 | 73 |
To CGI escape output (for example when creating a link), use the Liquid `uri_escape` filter, like so: |
72 | 74 |
|
@@ -80,7 +82,7 @@ module Agents |
||
80 | 82 |
after_save :clear_matchers |
81 | 83 |
|
82 | 84 |
def validate_options |
83 |
- errors.add(:base, "instructions, mode, skip_agent, and skip_created_at all need to be present.") unless options['instructions'].present? && options['mode'].present? && options['skip_agent'].present? && options['skip_created_at'].present? |
|
85 |
+ errors.add(:base, "instructions, mode, and skip_created_at all need to be present.") unless options['instructions'].present? && options['mode'].present? && options['skip_created_at'].present? |
|
84 | 86 |
|
85 | 87 |
validate_matchers |
86 | 88 |
end |
@@ -89,11 +91,11 @@ module Agents |
||
89 | 91 |
{ |
90 | 92 |
'instructions' => { |
91 | 93 |
'message' => "You received a text {{text}} from {{fields.from}}", |
94 |
+ 'agent' => "{{agent.type}}", |
|
92 | 95 |
'some_other_field' => "Looks like the weather is going to be {{fields.weather}}" |
93 | 96 |
}, |
94 | 97 |
'matchers' => [], |
95 | 98 |
'mode' => "clean", |
96 |
- 'skip_agent' => "false", |
|
97 | 99 |
'skip_created_at' => "false" |
98 | 100 |
} |
99 | 101 |
end |
@@ -105,10 +107,9 @@ module Agents |
||
105 | 107 |
def receive(incoming_events) |
106 | 108 |
incoming_events.each do |event| |
107 | 109 |
payload = perform_matching(event.payload) |
108 |
- opts = interpolated(payload) |
|
110 |
+ opts = interpolated(event.to_liquid(payload)) |
|
109 | 111 |
formatted_event = opts['mode'].to_s == "merge" ? event.payload.dup : {} |
110 | 112 |
formatted_event.merge! opts['instructions'] |
111 |
- formatted_event['agent'] = Agent.find(event.agent_id).type.slice!(8..-1) unless opts['skip_agent'].to_s == "true" |
|
112 | 113 |
formatted_event['created_at'] = event.created_at unless opts['skip_created_at'].to_s == "true" |
113 | 114 |
create_event :payload => formatted_event |
114 | 115 |
end |
@@ -0,0 +1,105 @@ |
||
1 |
+require 'json' |
|
2 |
+ |
|
3 |
+module Agents |
|
4 |
+ class GoogleCalendarPublishAgent < Agent |
|
5 |
+ cannot_be_scheduled! |
|
6 |
+ |
|
7 |
+ description <<-MD |
|
8 |
+ The GoogleCalendarPublishAgent creates events on your google calendar. |
|
9 |
+ |
|
10 |
+ This agent relies on service accounts, rather than oauth. |
|
11 |
+ |
|
12 |
+ Setup: |
|
13 |
+ |
|
14 |
+ 1. Visit [the google api console](https://code.google.com/apis/console/b/0/) |
|
15 |
+ 2. New project -> Huginn |
|
16 |
+ 3. APIs & Auth -> Enable google calendar |
|
17 |
+ 4. Credentials -> Create new Client ID -> Service Account |
|
18 |
+ 5. Persist the generated private key to a path, ie: `/home/hugin/a822ccdefac89fac6330f95039c492dfa3ce6843.p12` |
|
19 |
+ 6. Grant access via google calendar UI to the service account email address for each calendar you wish to manage. For a whole google apps domain, you can [delegate authority](https://developers.google.com/+/domains/authentication/delegation) |
|
20 |
+ |
|
21 |
+ |
|
22 |
+ Agent Configuration: |
|
23 |
+ |
|
24 |
+ `calendar_id` - The id the calendar you want to publish to. Typically your google account email address. |
|
25 |
+ |
|
26 |
+ `google` A hash of configuration options for the agent. |
|
27 |
+ |
|
28 |
+ `google` `service_account_email` - The authorised service account. |
|
29 |
+ |
|
30 |
+ `google` `key_file` - The path to the key file. |
|
31 |
+ |
|
32 |
+ `google` `key_secret` - The secret for the key, typically 'notasecret' |
|
33 |
+ |
|
34 |
+ |
|
35 |
+ |
|
36 |
+ Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent. |
|
37 |
+ |
|
38 |
+ Use it with a trigger agent to shape your payload! |
|
39 |
+ |
|
40 |
+ A hash of event details. See the [Google Calendar API docs](https://developers.google.com/google-apps/calendar/v3/reference/events/insert) |
|
41 |
+ |
|
42 |
+ Example payload for trigger agent: |
|
43 |
+ <pre><code>{ |
|
44 |
+ "message": { |
|
45 |
+ "visibility": "default", |
|
46 |
+ "summary": "Awesome event", |
|
47 |
+ "description": "An example event with text. Pro tip: DateTimes are in RFC3339", |
|
48 |
+ "start": { |
|
49 |
+ "dateTime": "2014-10-02T10:00:00-05:00" |
|
50 |
+ }, |
|
51 |
+ "end": { |
|
52 |
+ "dateTime": "2014-10-02T11:00:00-05:00" |
|
53 |
+ } |
|
54 |
+ } |
|
55 |
+ }</code></pre> |
|
56 |
+ MD |
|
57 |
+ |
|
58 |
+ event_description <<-MD |
|
59 |
+ { |
|
60 |
+ 'success' => true, |
|
61 |
+ 'published_calendar_event' => { |
|
62 |
+ .... |
|
63 |
+ }, |
|
64 |
+ 'agent_id' => 1234, |
|
65 |
+ 'event_id' => 3432, |
|
66 |
+ } |
|
67 |
+ MD |
|
68 |
+ |
|
69 |
+ def validate_options |
|
70 |
+ errors.add(:base, "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present? |
|
71 |
+ end |
|
72 |
+ |
|
73 |
+ def working? |
|
74 |
+ event_created_within?(options['expected_update_period_in_days']) && most_recent_event && most_recent_event.payload['success'] == true && !recent_error_logs? |
|
75 |
+ end |
|
76 |
+ |
|
77 |
+ def default_options |
|
78 |
+ { |
|
79 |
+ 'expected_update_period_in_days' => "10", |
|
80 |
+ 'calendar_id' => 'you@email.com', |
|
81 |
+ 'google' => { |
|
82 |
+ 'key_file' => '/path/to/private.key', |
|
83 |
+ 'key_secret' => 'notasecret', |
|
84 |
+ 'service_account_email' => '' |
|
85 |
+ } |
|
86 |
+ } |
|
87 |
+ end |
|
88 |
+ |
|
89 |
+ def receive(incoming_events) |
|
90 |
+ incoming_events.each do |event| |
|
91 |
+ calendar = GoogleCalendar.new(options, Rails.logger) |
|
92 |
+ |
|
93 |
+ calendar_event = JSON.parse(calendar.publish_as(options['calendar_id'], event.payload["message"]).response.body) |
|
94 |
+ |
|
95 |
+ create_event :payload => { |
|
96 |
+ 'success' => true, |
|
97 |
+ 'published_calendar_event' => calendar_event, |
|
98 |
+ 'agent_id' => event.agent_id, |
|
99 |
+ 'event_id' => event.id |
|
100 |
+ } |
|
101 |
+ end |
|
102 |
+ end |
|
103 |
+ end |
|
104 |
+end |
|
105 |
+ |
@@ -51,7 +51,7 @@ module Agents |
||
51 | 51 |
message = (event.payload['message'] || event.payload['text']).to_s |
52 | 52 |
subject = event.payload['subject'].to_s |
53 | 53 |
if message.present? && subject.present? |
54 |
- log "Sending Growl notification '#{subject}': '#{message}' to #{interpolated(event.payload)['growl_server']} with event #{event.id}" |
|
54 |
+ log "Sending Growl notification '#{subject}': '#{message}' to #{interpolated(event)['growl_server']} with event #{event.id}" |
|
55 | 55 |
notify_growl(subject,message) |
56 | 56 |
else |
57 | 57 |
log "Event #{event.id} not sent, message and subject expected" |
@@ -59,4 +59,4 @@ module Agents |
||
59 | 59 |
end |
60 | 60 |
end |
61 | 61 |
end |
62 |
-end |
|
62 |
+end |
@@ -42,7 +42,7 @@ module Agents |
||
42 | 42 |
def receive(incoming_events) |
43 | 43 |
client = HipChat::Client.new(interpolated[:auth_token]) |
44 | 44 |
incoming_events.each do |event| |
45 |
- mo = interpolated(event.payload) |
|
45 |
+ mo = interpolated(event) |
|
46 | 46 |
client[mo[:room_name]].send(mo[:username], mo[:message], :notify => mo[:notify].to_s == 'true' ? 1 : 0, :color => mo[:color]) |
47 | 47 |
end |
48 | 48 |
end |
@@ -11,7 +11,9 @@ module Agents |
||
11 | 11 |
description <<-MD |
12 | 12 |
|
13 | 13 |
The ImapFolderAgent checks an IMAP server in specified folders |
14 |
- and creates Events based on new unread mails. |
|
14 |
+ and creates Events based on new mails found since the last run. |
|
15 |
+ In the first visit to a foler, this agent only checks for the |
|
16 |
+ initial status and does not create events. |
|
15 | 17 |
|
16 | 18 |
Specify an IMAP server to connect with `host`, and set `ssl` to |
17 | 19 |
true if the server supports IMAP over SSL. Specify `port` if |
@@ -65,6 +67,13 @@ module Agents |
||
65 | 67 |
body. The default value is `['text/plain', 'text/enriched', |
66 | 68 |
'text/html']`. |
67 | 69 |
|
70 |
+ - "is_unread" |
|
71 |
+ |
|
72 |
+ Setting this to true or false means only mails that is |
|
73 |
+ marked as unread or read respectively, are selected. |
|
74 |
+ |
|
75 |
+ If this key is unspecified or set to null, it is ignored. |
|
76 |
+ |
|
68 | 77 |
- "has_attachment" |
69 | 78 |
|
70 | 79 |
Setting this to true or false means only mails that does or does |
@@ -74,13 +83,16 @@ module Agents |
||
74 | 83 |
|
75 | 84 |
Set `mark_as_read` to true to mark found mails as read. |
76 | 85 |
|
77 |
- Each agent instance memorizes a list of unread mails that are |
|
78 |
- found in the last run, so even if you change a set of conditions |
|
79 |
- so that it matches mails that are missed previously, they will |
|
80 |
- not show up as new events. Also, in order to avoid duplicated |
|
81 |
- notification it keeps a list of Message-Id's of 100 most recent |
|
82 |
- mails, so if multiple mails of the same Message-Id are found, |
|
83 |
- you will only see one event out of them. |
|
86 |
+ Each agent instance memorizes the highest UID of mails that are |
|
87 |
+ found in the last run for each watched folder, so even if you |
|
88 |
+ change a set of conditions so that it matches mails that are |
|
89 |
+ missed previously, or if you alter the flag status of already |
|
90 |
+ found mails, they will not show up as new events. |
|
91 |
+ |
|
92 |
+ Also, in order to avoid duplicated notification it keeps a list |
|
93 |
+ of Message-Id's of 100 most recent mails, so if multiple mails |
|
94 |
+ of the same Message-Id are found, you will only see one event |
|
95 |
+ out of them. |
|
84 | 96 |
MD |
85 | 97 |
|
86 | 98 |
event_description <<-MD |
@@ -138,9 +150,7 @@ module Agents |
||
138 | 150 |
|
139 | 151 |
%w[ssl mark_as_read].each { |key| |
140 | 152 |
if options[key].present? |
141 |
- case options[key] |
|
142 |
- when true, false |
|
143 |
- else |
|
153 |
+ if boolify(options[key]).nil? |
|
144 | 154 |
errors.add(:base, '%s must be a boolean value' % key) |
145 | 155 |
end |
146 | 156 |
end |
@@ -173,7 +183,6 @@ module Agents |
||
173 | 183 |
end |
174 | 184 |
|
175 | 185 |
case conditions = options['conditions'] |
176 |
- when nil |
|
177 | 186 |
when Hash |
178 | 187 |
conditions.each { |key, value| |
179 | 188 |
value.present? or next |
@@ -202,8 +211,8 @@ module Agents |
||
202 | 211 |
errors.add(:base, 'conditions.%s contains a non-string object' % key) |
203 | 212 |
end |
204 | 213 |
} |
205 |
- when 'has_attachment' |
|
206 |
- case value |
|
214 |
+ when 'is_unread', 'has_attachment' |
|
215 |
+ case boolify(value) |
|
207 | 216 |
when true, false |
208 | 217 |
else |
209 | 218 |
errors.add(:base, 'conditions.%s must be a boolean value or null' % key) |
@@ -220,22 +229,8 @@ module Agents |
||
220 | 229 |
end |
221 | 230 |
|
222 | 231 |
def check |
223 |
- # 'seen' keeps a hash of { uidvalidity => uids, ... } which |
|
224 |
- # lists unread mails in watched folders. |
|
225 |
- seen = memory['seen'] || {} |
|
226 |
- new_seen = Hash.new { |hash, key| |
|
227 |
- hash[key] = [] |
|
228 |
- } |
|
229 |
- |
|
230 |
- # 'notified' keeps an array of message-ids of {IDCACHE_SIZE} |
|
231 |
- # most recent notified mails. |
|
232 |
- notified = memory['notified'] || [] |
|
233 |
- |
|
234 |
- each_unread_mail { |mail| |
|
235 |
- new_seen[mail.uidvalidity] << mail.uid |
|
236 |
- |
|
237 |
- next if (uids = seen[mail.uidvalidity]) && uids.include?(mail.uid) |
|
238 |
- |
|
232 |
+ each_unread_mail { |mail, notified| |
|
233 |
+ message_id = mail.message_id |
|
239 | 234 |
body_parts = mail.body_parts(mime_types) |
240 | 235 |
matched_part = nil |
241 | 236 |
matches = {} |
@@ -274,14 +269,18 @@ module Agents |
||
274 | 269 |
} |
275 | 270 |
} |
276 | 271 |
when 'has_attachment' |
277 |
- value == mail.has_attachment? |
|
272 |
+ boolify(value) == mail.has_attachment? |
|
273 |
+ when 'is_unread' |
|
274 |
+ true # already filtered out by each_unread_mail |
|
278 | 275 |
else |
279 | 276 |
log 'Unknown condition key ignored: %s' % key |
280 | 277 |
true |
281 | 278 |
end |
282 | 279 |
} or next |
283 | 280 |
|
284 |
- unless notified.include?(mail.message_id) |
|
281 |
+ if notified.include?(mail.message_id) |
|
282 |
+ log 'Ignoring mail: %s (already notified)' % message_id |
|
283 |
+ else |
|
285 | 284 |
matched_part ||= body_parts.first |
286 | 285 |
|
287 | 286 |
if matched_part |
@@ -292,6 +291,8 @@ module Agents |
||
292 | 291 |
body = '' |
293 | 292 |
end |
294 | 293 |
|
294 |
+ log 'Emitting an event for mail: %s' % message_id |
|
295 |
+ |
|
295 | 296 |
create_event :payload => { |
296 | 297 |
'folder' => mail.folder, |
297 | 298 |
'subject' => mail.subject, |
@@ -308,43 +309,86 @@ module Agents |
||
308 | 309 |
notified << mail.message_id if mail.message_id |
309 | 310 |
end |
310 | 311 |
|
311 |
- if interpolated['mark_as_read'] |
|
312 |
+ if boolify(interpolated['mark_as_read']) |
|
312 | 313 |
log 'Marking as read' |
313 | 314 |
mail.mark_as_read |
314 | 315 |
end |
315 | 316 |
} |
316 |
- |
|
317 |
- notified.slice!(0...-IDCACHE_SIZE) if notified.size > IDCACHE_SIZE |
|
318 |
- |
|
319 |
- memory['seen'] = new_seen |
|
320 |
- memory['notified'] = notified |
|
321 |
- save! |
|
322 | 317 |
end |
323 | 318 |
|
324 | 319 |
def each_unread_mail |
325 | 320 |
host, port, ssl, username = interpolated.values_at(:host, :port, :ssl, :username) |
321 |
+ ssl = boolify(ssl) |
|
322 |
+ port = (Integer(port) if port.present?) |
|
326 | 323 |
|
327 | 324 |
log "Connecting to #{host}#{':%d' % port if port}#{' via SSL' if ssl}" |
328 |
- Client.open(host, Integer(port), ssl) { |imap| |
|
325 |
+ Client.open(host, port, ssl) { |imap| |
|
329 | 326 |
log "Logging in as #{username}" |
330 | 327 |
imap.login(username, interpolated[:password]) |
331 | 328 |
|
329 |
+ # 'lastseen' keeps a hash of { uidvalidity => lastseenuid, ... } |
|
330 |
+ lastseen, seen = self.lastseen, self.make_seen |
|
331 |
+ |
|
332 |
+ # 'notified' keeps an array of message-ids of {IDCACHE_SIZE} |
|
333 |
+ # most recent notified mails. |
|
334 |
+ notified = self.notified |
|
335 |
+ |
|
332 | 336 |
interpolated['folders'].each { |folder| |
333 | 337 |
log "Selecting the folder: %s" % folder |
334 | 338 |
|
335 | 339 |
imap.select(folder) |
340 |
+ uidvalidity = imap.uidvalidity |
|
341 |
+ |
|
342 |
+ lastseenuid = lastseen[uidvalidity] |
|
336 | 343 |
|
337 |
- unseen = imap.search('UNSEEN') |
|
344 |
+ if lastseenuid.nil? |
|
345 |
+ maxseq = imap.responses['EXISTS'].last |
|
346 |
+ |
|
347 |
+ log "Recording the initial status: %s" % pluralize(maxseq, 'existing mail') |
|
348 |
+ |
|
349 |
+ if maxseq > 0 |
|
350 |
+ seen[uidvalidity] = imap.fetch(maxseq, 'UID').last.attr['UID'] |
|
351 |
+ end |
|
338 | 352 |
|
339 |
- if unseen.empty? |
|
340 |
- log "No unread mails" |
|
341 | 353 |
next |
342 | 354 |
end |
343 | 355 |
|
344 |
- imap.fetch_mails(unseen).each { |mail| |
|
345 |
- yield mail |
|
356 |
+ seen[uidvalidity] = lastseenuid |
|
357 |
+ is_unread = boolify(interpolated['conditions']['is_unread']) |
|
358 |
+ |
|
359 |
+ uids = imap.uid_fetch((lastseenuid + 1)..-1, 'FLAGS'). |
|
360 |
+ each_with_object([]) { |data, ret| |
|
361 |
+ uid, flags = data.attr.values_at('UID', 'FLAGS') |
|
362 |
+ seen[uidvalidity] = uid |
|
363 |
+ next if uid <= lastseenuid |
|
364 |
+ |
|
365 |
+ case is_unread |
|
366 |
+ when nil, !flags.include?(:Seen) |
|
367 |
+ ret << uid |
|
368 |
+ end |
|
369 |
+ } |
|
370 |
+ |
|
371 |
+ log pluralize(uids.size, |
|
372 |
+ case is_unread |
|
373 |
+ when true |
|
374 |
+ 'new unread mail' |
|
375 |
+ when false |
|
376 |
+ 'new read mail' |
|
377 |
+ else |
|
378 |
+ 'new mail' |
|
379 |
+ end) |
|
380 |
+ |
|
381 |
+ next if uids.empty? |
|
382 |
+ |
|
383 |
+ imap.uid_fetch_mails(uids).each { |mail| |
|
384 |
+ yield mail, notified |
|
346 | 385 |
} |
347 | 386 |
} |
387 |
+ |
|
388 |
+ self.notified = notified |
|
389 |
+ self.lastseen = seen |
|
390 |
+ |
|
391 |
+ save! |
|
348 | 392 |
} |
349 | 393 |
ensure |
350 | 394 |
log 'Connection closed' |
@@ -354,6 +398,27 @@ module Agents |
||
354 | 398 |
interpolated['mime_types'] || %w[text/plain text/enriched text/html] |
355 | 399 |
end |
356 | 400 |
|
401 |
+ def lastseen |
|
402 |
+ Seen.new(memory['lastseen']) |
|
403 |
+ end |
|
404 |
+ |
|
405 |
+ def lastseen= value |
|
406 |
+ memory.delete('seen') # obsolete key |
|
407 |
+ memory['lastseen'] = value |
|
408 |
+ end |
|
409 |
+ |
|
410 |
+ def make_seen |
|
411 |
+ Seen.new |
|
412 |
+ end |
|
413 |
+ |
|
414 |
+ def notified |
|
415 |
+ Notified.new(memory['notified']) |
|
416 |
+ end |
|
417 |
+ |
|
418 |
+ def notified= value |
|
419 |
+ memory['notified'] = value |
|
420 |
+ end |
|
421 |
+ |
|
357 | 422 |
private |
358 | 423 |
|
359 | 424 |
def is_positive_integer?(value) |
@@ -366,6 +431,10 @@ module Agents |
||
366 | 431 |
File.fnmatch?(pattern, value, FNM_FLAGS) |
367 | 432 |
end |
368 | 433 |
|
434 |
+ def pluralize(count, noun) |
|
435 |
+ "%d %s" % [count, noun.pluralize(count)] |
|
436 |
+ end |
|
437 |
+ |
|
369 | 438 |
class Client < ::Net::IMAP |
370 | 439 |
class << self |
371 | 440 |
def open(host, port, ssl) |
@@ -376,19 +445,52 @@ module Agents |
||
376 | 445 |
end |
377 | 446 |
end |
378 | 447 |
|
448 |
+ attr_reader :uidvalidity |
|
449 |
+ |
|
379 | 450 |
def select(folder) |
380 | 451 |
ret = super(@folder = folder) |
381 | 452 |
@uidvalidity = responses['UIDVALIDITY'].last |
382 | 453 |
ret |
383 | 454 |
end |
384 | 455 |
|
385 |
- def fetch_mails(set) |
|
386 |
- fetch(set, %w[UID RFC822.HEADER]).map { |data| |
|
456 |
+ def uid_fetch_mails(set) |
|
457 |
+ uid_fetch(set, 'RFC822.HEADER').map { |data| |
|
387 | 458 |
Message.new(self, data, folder: @folder, uidvalidity: @uidvalidity) |
388 | 459 |
} |
389 | 460 |
end |
390 | 461 |
end |
391 | 462 |
|
463 |
+ class Seen < Hash |
|
464 |
+ def initialize(hash = nil) |
|
465 |
+ super() |
|
466 |
+ if hash |
|
467 |
+ # Deserialize a JSON hash which keys are strings |
|
468 |
+ hash.each { |uidvalidity, uid| |
|
469 |
+ self[uidvalidity.to_i] = uid |
|
470 |
+ } |
|
471 |
+ end |
|
472 |
+ end |
|
473 |
+ |
|
474 |
+ def []=(uidvalidity, uid) |
|
475 |
+ # Update only if the new value is larger than the current value |
|
476 |
+ if (curr = self[uidvalidity]).nil? || curr <= uid |
|
477 |
+ super |
|
478 |
+ end |
|
479 |
+ end |
|
480 |
+ end |
|
481 |
+ |
|
482 |
+ class Notified < Array |
|
483 |
+ def initialize(array = nil) |
|
484 |
+ super() |
|
485 |
+ replace(array) if array |
|
486 |
+ end |
|
487 |
+ |
|
488 |
+ def <<(value) |
|
489 |
+ slice!(0...-IDCACHE_SIZE) if size > IDCACHE_SIZE |
|
490 |
+ super |
|
491 |
+ end |
|
492 |
+ end |
|
493 |
+ |
|
392 | 494 |
class Message < SimpleDelegator |
393 | 495 |
DEFAULT_BODY_MIME_TYPES = %w[text/plain text/enriched text/html] |
394 | 496 |
|
@@ -60,7 +60,7 @@ module Agents |
||
60 | 60 |
end |
61 | 61 |
|
62 | 62 |
def body(event) |
63 |
- interpolated(event.payload)['message'] |
|
63 |
+ interpolated(event)['message'] |
|
64 | 64 |
end |
65 | 65 |
end |
66 | 66 |
end |
@@ -106,7 +106,7 @@ module Agents |
||
106 | 106 |
def receive(incoming_events) |
107 | 107 |
mqtt_client.connect do |c| |
108 | 108 |
incoming_events.each do |event| |
109 |
- c.publish(interpolated(event.payload)['topic'], event.payload) |
|
109 |
+ c.publish(interpolated(event)['topic'], event) |
|
110 | 110 |
end |
111 | 111 |
|
112 | 112 |
c.disconnect |
@@ -136,4 +136,4 @@ module Agents |
||
136 | 136 |
end |
137 | 137 |
|
138 | 138 |
end |
139 |
-end |
|
139 |
+end |
@@ -38,7 +38,7 @@ module Agents |
||
38 | 38 |
'expected_receive_period_in_days' => "2", |
39 | 39 |
'group_by_path' => "filter", |
40 | 40 |
'value_path' => "count", |
41 |
- 'message' => "A peak was found" |
|
41 |
+ 'message' => "A peak of {{count}} was found in {{filter}}" |
|
42 | 42 |
} |
43 | 43 |
end |
44 | 44 |
|
@@ -67,7 +67,7 @@ module Agents |
||
67 | 67 |
if newest_value > average_value + std_multiple * standard_deviation |
68 | 68 |
memory['peaks'][group] << newest_time |
69 | 69 |
memory['peaks'][group].reject! { |p| p <= newest_time - window_duration } |
70 |
- create_event :payload => { 'message' => interpolated(event.payload)['message'], 'peak' => newest_value, 'peak_time' => newest_time, 'grouped_by' => group.to_s } |
|
70 |
+ create_event :payload => { 'message' => interpolated(event)['message'], 'peak' => newest_value, 'peak_time' => newest_time, 'grouped_by' => group.to_s } |
|
71 | 71 |
end |
72 | 72 |
end |
73 | 73 |
end |
@@ -127,4 +127,4 @@ module Agents |
||
127 | 127 |
memory['data'][group].reject! { |value, time| time <= newest_time - window_duration } |
128 | 128 |
end |
129 | 129 |
end |
130 |
-end |
|
130 |
+end |
@@ -5,10 +5,14 @@ module Agents |
||
5 | 5 |
default_schedule "never" |
6 | 6 |
|
7 | 7 |
description <<-MD |
8 |
- A PostAgent receives events from other agents (or runs periodically), merges those events with the contents of `payload`, and sends the results as POST (or GET) requests to a specified url. |
|
8 |
+ A PostAgent receives events from other agents (or runs periodically), merges those events with the [Liquid-interpolated](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) contents of `payload`, and sends the results as POST (or GET) requests to a specified url. To skip merging in the incoming event, but still send the interpolated payload, set `no_merge` to `true`. |
|
9 | 9 |
|
10 | 10 |
The `post_url` field must specify where you would like to send requests. Please include the URI scheme (`http` or `https`). |
11 | 11 |
|
12 |
+ The `method` used can be any of `get`, `post`, `put`, `patch`, and `delete`. |
|
13 |
+ |
|
14 |
+ By default, non-GETs will be sent with form encoding (`application/x-www-form-urlencoded`). Change `content_type` to `json` to send JSON instead. |
|
15 |
+ |
|
12 | 16 |
The `headers` field is optional. When present, it should be a hash of headers to send with the request. |
13 | 17 |
MD |
14 | 18 |
|
@@ -17,10 +21,12 @@ module Agents |
||
17 | 21 |
def default_options |
18 | 22 |
{ |
19 | 23 |
'post_url' => "http://www.example.com", |
20 |
- 'expected_receive_period_in_days' => 1, |
|
24 |
+ 'expected_receive_period_in_days' => '1', |
|
25 |
+ 'content_type' => 'form', |
|
21 | 26 |
'method' => 'post', |
22 | 27 |
'payload' => { |
23 |
- 'key' => 'value' |
|
28 |
+ 'key' => 'value', |
|
29 |
+ 'something' => 'the event contained {{ somekey }}' |
|
24 | 30 |
}, |
25 | 31 |
'headers' => {} |
26 | 32 |
} |
@@ -47,8 +53,12 @@ module Agents |
||
47 | 53 |
errors.add(:base, "if provided, payload must be a hash") |
48 | 54 |
end |
49 | 55 |
|
50 |
- unless %w[post get].include?(method) |
|
51 |
- errors.add(:base, "method must be 'post' or 'get'") |
|
56 |
+ unless %w[post get put delete patch].include?(method) |
|
57 |
+ errors.add(:base, "method must be 'post', 'get', 'put', 'delete', or 'patch'") |
|
58 |
+ end |
|
59 |
+ |
|
60 |
+ if options['no_merge'].present? && !%[true false].include?(options['no_merge'].to_s) |
|
61 |
+ errors.add(:base, "if provided, no_merge must be 'true' or 'false'") |
|
52 | 62 |
end |
53 | 63 |
|
54 | 64 |
unless headers.is_a?(Hash) |
@@ -58,7 +68,12 @@ module Agents |
||
58 | 68 |
|
59 | 69 |
def receive(incoming_events) |
60 | 70 |
incoming_events.each do |event| |
61 |
- handle (interpolated(event.payload)['payload'].presence || {}).merge(event.payload) |
|
71 |
+ outgoing = interpolated(event)['payload'].presence || {} |
|
72 |
+ if interpolated['no_merge'].to_s == 'true' |
|
73 |
+ handle outgoing, event.payload |
|
74 |
+ else |
|
75 |
+ handle outgoing.merge(event.payload), event.payload |
|
76 |
+ end |
|
62 | 77 |
end |
63 | 78 |
end |
64 | 79 |
|
@@ -66,35 +81,48 @@ module Agents |
||
66 | 81 |
handle interpolated['payload'].presence || {} |
67 | 82 |
end |
68 | 83 |
|
69 |
- def generate_uri(params = nil) |
|
70 |
- uri = URI interpolated[:post_url] |
|
84 |
+ def generate_uri(params = nil, payload = {}) |
|
85 |
+ uri = URI interpolated(payload)[:post_url] |
|
71 | 86 |
uri.query = URI.encode_www_form(Hash[URI.decode_www_form(uri.query || '')].merge(params)) if params |
72 | 87 |
uri |
73 | 88 |
end |
74 | 89 |
|
75 | 90 |
private |
76 | 91 |
|
77 |
- def handle(data) |
|
92 |
+ def handle(data, payload = {}) |
|
78 | 93 |
if method == 'post' |
79 |
- post_data(data) |
|
94 |
+ post_data(data, payload, Net::HTTP::Post) |
|
95 |
+ elsif method == 'put' |
|
96 |
+ post_data(data, payload, Net::HTTP::Put) |
|
97 |
+ elsif method == 'delete' |
|
98 |
+ post_data(data, payload, Net::HTTP::Delete) |
|
99 |
+ elsif method == 'patch' |
|
100 |
+ post_data(data, payload, Net::HTTP::Patch) |
|
80 | 101 |
elsif method == 'get' |
81 |
- get_data(data) |
|
102 |
+ get_data(data, payload) |
|
82 | 103 |
else |
83 | 104 |
error "Invalid method '#{method}'" |
84 | 105 |
end |
85 | 106 |
end |
86 | 107 |
|
87 |
- def post_data(data) |
|
88 |
- uri = generate_uri |
|
89 |
- req = Net::HTTP::Post.new(uri.request_uri, headers) |
|
90 |
- req.form_data = data |
|
108 |
+ def post_data(data, payload, request_type = Net::HTTP::Post) |
|
109 |
+ uri = generate_uri(nil, payload) |
|
110 |
+ req = request_type.new(uri.request_uri, headers) |
|
111 |
+ |
|
112 |
+ if interpolated(payload)['content_type'] == 'json' |
|
113 |
+ req.set_content_type('application/json', 'charset' => 'utf-8') |
|
114 |
+ req.body = data.to_json |
|
115 |
+ else |
|
116 |
+ req.form_data = data |
|
117 |
+ end |
|
118 |
+ |
|
91 | 119 |
Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == "https") { |http| http.request(req) } |
92 | 120 |
end |
93 | 121 |
|
94 |
- def get_data(data) |
|
95 |
- uri = generate_uri(data) |
|
122 |
+ def get_data(data, payload) |
|
123 |
+ uri = generate_uri(data, payload) |
|
96 | 124 |
req = Net::HTTP::Get.new(uri.request_uri, headers) |
97 | 125 |
Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == "https") { |http| http.request(req) } |
98 | 126 |
end |
99 | 127 |
end |
100 |
-end |
|
128 |
+end |
@@ -49,7 +49,7 @@ module Agents |
||
49 | 49 |
private |
50 | 50 |
|
51 | 51 |
def query_options(event) |
52 |
- mo = interpolated(event.payload) |
|
52 |
+ mo = interpolated(event) |
|
53 | 53 |
{ |
54 | 54 |
:basic_auth => {:username => mo[:api_key], :password => ''}, |
55 | 55 |
:body => {:device_iden => mo[:device_id], :title => mo[:title], :body => mo[:body], :type => 'note'} |
@@ -58,7 +58,7 @@ module Agents |
||
58 | 58 |
|
59 | 59 |
def receive(incoming_events) |
60 | 60 |
incoming_events.each do |event| |
61 |
- payload_interpolated = interpolated(event.payload) |
|
61 |
+ payload_interpolated = interpolated(event) |
|
62 | 62 |
message = (event.payload['message'].presence || event.payload['text'].presence || payload_interpolated['message']).to_s |
63 | 63 |
if message.present? |
64 | 64 |
post_params = { |
@@ -0,0 +1,89 @@ |
||
1 |
+require 'rss' |
|
2 |
+require 'feed-normalizer' |
|
3 |
+ |
|
4 |
+module Agents |
|
5 |
+ class RssAgent < Agent |
|
6 |
+ include WebRequestConcern |
|
7 |
+ |
|
8 |
+ cannot_receive_events! |
|
9 |
+ default_schedule "every_1d" |
|
10 |
+ |
|
11 |
+ description do |
|
12 |
+ <<-MD |
|
13 |
+ This Agent consumes RSS feeds and emits events when they change. |
|
14 |
+ |
|
15 |
+ (If you want to *output* an RSS feed, use the DataOutputAgent. Also, you can technically parse RSS and XML feeds |
|
16 |
+ with the WebsiteAgent as well. See [this example](https://github.com/cantino/huginn/wiki/Agent-configuration-examples#itunes-trailers).) |
|
17 |
+ |
|
18 |
+ Options: |
|
19 |
+ |
|
20 |
+ * `url` - The URL of the RSS feed. |
|
21 |
+ * `clean` - Attempt to use [feed-normalizer](https://github.com/aasmith/feed-normalizer)'s' `clean!` method to cleanup HTML in the feed. Set to `true` to use. |
|
22 |
+ * `expected_update_period_in_days` - How often you expect this RSS feed to change. If more than this amount of time passes without an update, the Agent will mark itself as not working. |
|
23 |
+ MD |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ def default_options |
|
27 |
+ { |
|
28 |
+ 'expected_update_period_in_days' => "5", |
|
29 |
+ 'clean' => 'false', |
|
30 |
+ 'url' => "https://github.com/cantino/huginn/commits/master.atom" |
|
31 |
+ } |
|
32 |
+ end |
|
33 |
+ |
|
34 |
+ def working? |
|
35 |
+ event_created_within?((interpolated['expected_update_period_in_days'].presence || 10).to_i) && !recent_error_logs? |
|
36 |
+ end |
|
37 |
+ |
|
38 |
+ def validate_options |
|
39 |
+ errors.add(:base, "url is required") unless options['url'].present? |
|
40 |
+ |
|
41 |
+ unless options['expected_update_period_in_days'].present? && options['expected_update_period_in_days'].to_i > 0 |
|
42 |
+ errors.add(:base, "Please provide 'expected_update_period_in_days' to indicate how many days can pass without an update before this Agent is considered to not be working") |
|
43 |
+ end |
|
44 |
+ |
|
45 |
+ validate_web_request_options! |
|
46 |
+ end |
|
47 |
+ |
|
48 |
+ def check |
|
49 |
+ response = faraday.get(interpolated['url']) |
|
50 |
+ if response.success? |
|
51 |
+ feed = FeedNormalizer::FeedNormalizer.parse(response.body) |
|
52 |
+ feed.clean! if interpolated['clean'] == 'true' |
|
53 |
+ created_event_count = 0 |
|
54 |
+ feed.entries.each do |entry| |
|
55 |
+ if check_and_track(entry.id) |
|
56 |
+ created_event_count += 1 |
|
57 |
+ create_event(:payload => { |
|
58 |
+ :id => entry.id, |
|
59 |
+ :date_published => entry.date_published, |
|
60 |
+ :last_updated => entry.last_updated, |
|
61 |
+ :urls => entry.urls, |
|
62 |
+ :description => entry.description, |
|
63 |
+ :content => entry.content, |
|
64 |
+ :title => entry.title, |
|
65 |
+ :authors => entry.authors, |
|
66 |
+ :categories => entry.categories |
|
67 |
+ }) |
|
68 |
+ end |
|
69 |
+ end |
|
70 |
+ log "Fetched #{interpolated['url']} and created #{created_event_count} event(s)." |
|
71 |
+ else |
|
72 |
+ error "Failed to fetch #{interpolated['url']}: #{response.inspect}" |
|
73 |
+ end |
|
74 |
+ end |
|
75 |
+ |
|
76 |
+ protected |
|
77 |
+ |
|
78 |
+ def check_and_track(entry_id) |
|
79 |
+ memory['seen_ids'] ||= [] |
|
80 |
+ if memory['seen_ids'].include?(entry_id) |
|
81 |
+ false |
|
82 |
+ else |
|
83 |
+ memory['seen_ids'].unshift entry_id |
|
84 |
+ memory['seen_ids'].pop if memory['seen_ids'].length > 500 |
|
85 |
+ true |
|
86 |
+ end |
|
87 |
+ end |
|
88 |
+ end |
|
89 |
+end |
@@ -61,7 +61,7 @@ module Agents |
||
61 | 61 |
|
62 | 62 |
def receive(incoming_events) |
63 | 63 |
incoming_events.each do |event| |
64 |
- handle(interpolated(event.payload), event) |
|
64 |
+ handle(interpolated(event), event) |
|
65 | 65 |
end |
66 | 66 |
end |
67 | 67 |
|
@@ -109,4 +109,4 @@ module Agents |
||
109 | 109 |
[result, errors, exit_status] |
110 | 110 |
end |
111 | 111 |
end |
112 |
-end |
|
112 |
+end |
@@ -57,7 +57,7 @@ module Agents |
||
57 | 57 |
|
58 | 58 |
def receive(incoming_events) |
59 | 59 |
incoming_events.each do |event| |
60 |
- opts = interpolated(event.payload) |
|
60 |
+ opts = interpolated(event) |
|
61 | 61 |
slack_notifier.ping opts[:message], channel: opts[:channel], username: opts[:username] |
62 | 62 |
end |
63 | 63 |
end |
@@ -66,7 +66,7 @@ module Agents |
||
66 | 66 |
access_token = JSON.parse(response.body)["access_token"] |
67 | 67 |
incoming_events.each do |event| |
68 | 68 |
translated_event = {} |
69 |
- opts = interpolated(event.payload) |
|
69 |
+ opts = interpolated(event) |
|
70 | 70 |
opts['content'].each_pair do |key, value| |
71 | 71 |
translated_event[key] = translate(value.first, opts['to'], access_token) |
72 | 72 |
end |
@@ -57,7 +57,7 @@ module Agents |
||
57 | 57 |
def receive(incoming_events) |
58 | 58 |
incoming_events.each do |event| |
59 | 59 |
|
60 |
- opts = interpolated(event.payload) |
|
60 |
+ opts = interpolated(event) |
|
61 | 61 |
|
62 | 62 |
match = opts['rules'].all? do |rule| |
63 | 63 |
value_at_path = Utils.value_at(event['payload'], rule['path']) |
@@ -105,4 +105,4 @@ module Agents |
||
105 | 105 |
interpolated['keep_event'] == 'true' |
106 | 106 |
end |
107 | 107 |
end |
108 |
-end |
|
108 |
+end |
@@ -44,13 +44,13 @@ module Agents |
||
44 | 44 |
incoming_events.each do |event| |
45 | 45 |
message = (event.payload['message'].presence || event.payload['text'].presence || event.payload['sms'].presence).to_s |
46 | 46 |
if message.present? |
47 |
- if interpolated(event.payload)['receive_call'].to_s == 'true' |
|
47 |
+ if interpolated(event)['receive_call'].to_s == 'true' |
|
48 | 48 |
secret = SecureRandom.hex 3 |
49 | 49 |
memory['pending_calls'][secret] = message |
50 | 50 |
make_call secret |
51 | 51 |
end |
52 | 52 |
|
53 |
- if interpolated(event.payload)['receive_text'].to_s == 'true' |
|
53 |
+ if interpolated(event)['receive_text'].to_s == 'true' |
|
54 | 54 |
message = message.slice 0..160 |
55 | 55 |
send_message message |
56 | 56 |
end |
@@ -86,4 +86,4 @@ module Agents |
||
86 | 86 |
end |
87 | 87 |
end |
88 | 88 |
end |
89 |
-end |
|
89 |
+end |
@@ -37,7 +37,7 @@ module Agents |
||
37 | 37 |
incoming_events = incoming_events.first(20) |
38 | 38 |
end |
39 | 39 |
incoming_events.each do |event| |
40 |
- tweet_text = interpolated(event.payload)['message'] |
|
40 |
+ tweet_text = interpolated(event)['message'] |
|
41 | 41 |
begin |
42 | 42 |
tweet = publish_tweet tweet_text |
43 | 43 |
create_event :payload => { |
@@ -63,4 +63,4 @@ module Agents |
||
63 | 63 |
twitter.update(text) |
64 | 64 |
end |
65 | 65 |
end |
66 |
-end |
|
66 |
+end |
@@ -5,6 +5,7 @@ require 'date' |
||
5 | 5 |
|
6 | 6 |
module Agents |
7 | 7 |
class WebsiteAgent < Agent |
8 |
+ include WebRequestConcern |
|
8 | 9 |
|
9 | 10 |
default_schedule "every_12h" |
10 | 11 |
|
@@ -22,14 +23,16 @@ module Agents |
||
22 | 23 |
|
23 | 24 |
To tell the Agent how to parse the content, specify `extract` as a hash with keys naming the extractions and values of hashes. |
24 | 25 |
|
25 |
- When parsing HTML or XML, these sub-hashes specify how to extract with either a `css` CSS selector or a `xpath` XPath expression and either `"text": true` or `attr` pointing to an attribute name to grab. An example: |
|
26 |
+ When parsing HTML or XML, these sub-hashes specify how each extraction should be done. The Agent first selects a node set from the document for each extraction key by evaluating either a CSS selector in `css` or an XPath expression in `xpath`. It then evaluates an XPath expression in `value` on each node in the node set, converting the result into string. Here's an example: |
|
26 | 27 |
|
27 | 28 |
"extract": { |
28 |
- "url": { "css": "#comic img", "attr": "src" }, |
|
29 |
- "title": { "css": "#comic img", "attr": "title" }, |
|
30 |
- "body_text": { "css": "div.main", "text": true } |
|
29 |
+ "url": { "css": "#comic img", "value": "@src" }, |
|
30 |
+ "title": { "css": "#comic img", "value": "@title" }, |
|
31 |
+ "body_text": { "css": "div.main", "value": ".//text()" } |
|
31 | 32 |
} |
32 | 33 |
|
34 |
+ "@_attr_" is the XPath expression to extract the value of an attribute named _attr_ from a node, and ".//text()" is to extract all the enclosed texts. You can also use [XPath functions](http://www.w3.org/TR/xpath/#section-String-Functions) like `normalize-space` to strip and squeeze whitespace, `substring-after` to extract part of a text, and `translate` to remove comma from a formatted number, etc. Note that these functions take a string, not a node set, so what you may think would be written as `normalize-text(.//text())` should actually be `normalize-text(.)`. |
|
35 |
+ |
|
33 | 36 |
When parsing JSON, these sub-hashes specify [JSONPaths](http://goessner.net/articles/JsonPath/) to the values that you care about. For example: |
34 | 37 |
|
35 | 38 |
"extract": { |
@@ -69,9 +72,9 @@ module Agents |
||
69 | 72 |
'type' => "html", |
70 | 73 |
'mode' => "on_change", |
71 | 74 |
'extract' => { |
72 |
- 'url' => { 'css' => "#comic img", 'attr' => "src" }, |
|
73 |
- 'title' => { 'css' => "#comic img", 'attr' => "alt" }, |
|
74 |
- 'hovertext' => { 'css' => "#comic img", 'attr' => "title" } |
|
75 |
+ 'url' => { 'css' => "#comic img", 'value' => "@src" }, |
|
76 |
+ 'title' => { 'css' => "#comic img", 'value' => "@alt" }, |
|
77 |
+ 'hovertext' => { 'css' => "#comic img", 'value' => "@title" } |
|
75 | 78 |
} |
76 | 79 |
} |
77 | 80 |
end |
@@ -109,19 +112,7 @@ module Agents |
||
109 | 112 |
end |
110 | 113 |
end |
111 | 114 |
|
112 |
- if options['user_agent'].present? |
|
113 |
- errors.add(:base, "user_agent must be a string") unless options['user_agent'].is_a?(String) |
|
114 |
- end |
|
115 |
- |
|
116 |
- unless headers.is_a?(Hash) |
|
117 |
- errors.add(:base, "if provided, headers must be a hash") |
|
118 |
- end |
|
119 |
- |
|
120 |
- begin |
|
121 |
- basic_auth_credentials() |
|
122 |
- rescue => e |
|
123 |
- errors.add(:base, e.message) |
|
124 |
- end |
|
115 |
+ validate_web_request_options! |
|
125 | 116 |
end |
126 | 117 |
|
127 | 118 |
def check |
@@ -157,25 +148,27 @@ module Agents |
||
157 | 148 |
when css = extraction_details['css'] |
158 | 149 |
nodes = doc.css(css) |
159 | 150 |
when xpath = extraction_details['xpath'] |
151 |
+ doc.remove_namespaces! # ignore xmlns, useful when parsing atom feeds |
|
160 | 152 |
nodes = doc.xpath(xpath) |
161 | 153 |
else |
162 | 154 |
error '"css" or "xpath" is required for HTML or XML extraction' |
163 | 155 |
return |
164 | 156 |
end |
165 |
- unless Nokogiri::XML::NodeSet === nodes |
|
157 |
+ case nodes |
|
158 |
+ when Nokogiri::XML::NodeSet |
|
159 |
+ result = nodes.map { |node| |
|
160 |
+ case value = node.xpath(extraction_details['value']) |
|
161 |
+ when Float |
|
162 |
+ # Node#xpath() returns any numeric value as float; |
|
163 |
+ # convert it to integer as appropriate. |
|
164 |
+ value = value.to_i if value.to_i == value |
|
165 |
+ end |
|
166 |
+ value.to_s |
|
167 |
+ } |
|
168 |
+ else |
|
166 | 169 |
error "The result of HTML/XML extraction was not a NodeSet" |
167 | 170 |
return |
168 | 171 |
end |
169 |
- result = nodes.map { |node| |
|
170 |
- if extraction_details['attr'] |
|
171 |
- node.attr(extraction_details['attr']) |
|
172 |
- elsif extraction_details['text'] |
|
173 |
- node.text() |
|
174 |
- else |
|
175 |
- error '"attr" or "text" is required on HTML or XML extraction patterns' |
|
176 |
- return |
|
177 |
- end |
|
178 |
- } |
|
179 | 172 |
log "Extracting #{extraction_type} at #{xpath || css}: #{result}" |
180 | 173 |
end |
181 | 174 |
output[name] = result |
@@ -290,47 +283,5 @@ module Agents |
||
290 | 283 |
false |
291 | 284 |
end |
292 | 285 |
end |
293 |
- |
|
294 |
- def faraday |
|
295 |
- @faraday ||= Faraday.new { |builder| |
|
296 |
- builder.headers = headers if headers.length > 0 |
|
297 |
- |
|
298 |
- if (user_agent = interpolated['user_agent']).present? |
|
299 |
- builder.headers[:user_agent] = user_agent |
|
300 |
- end |
|
301 |
- |
|
302 |
- builder.use FaradayMiddleware::FollowRedirects |
|
303 |
- builder.request :url_encoded |
|
304 |
- if userinfo = basic_auth_credentials() |
|
305 |
- builder.request :basic_auth, *userinfo |
|
306 |
- end |
|
307 |
- |
|
308 |
- case backend = faraday_backend |
|
309 |
- when :typhoeus |
|
310 |
- require 'typhoeus/adapters/faraday' |
|
311 |
- end |
|
312 |
- builder.adapter backend |
|
313 |
- } |
|
314 |
- end |
|
315 |
- |
|
316 |
- def faraday_backend |
|
317 |
- ENV.fetch('FARADAY_HTTP_BACKEND', 'typhoeus').to_sym |
|
318 |
- end |
|
319 |
- |
|
320 |
- def basic_auth_credentials |
|
321 |
- case value = interpolated['basic_auth'] |
|
322 |
- when nil, '' |
|
323 |
- return nil |
|
324 |
- when Array |
|
325 |
- return value if value.size == 2 |
|
326 |
- when /:/ |
|
327 |
- return value.split(/:/, 2) |
|
328 |
- end |
|
329 |
- raise "bad value for basic_auth: #{value.inspect}" |
|
330 |
- end |
|
331 |
- |
|
332 |
- def headers |
|
333 |
- interpolated['headers'].presence || {} |
|
334 |
- end |
|
335 | 286 |
end |
336 | 287 |
end |
@@ -47,7 +47,7 @@ module Agents |
||
47 | 47 |
incoming_events = incoming_events.first(20) |
48 | 48 |
end |
49 | 49 |
incoming_events.each do |event| |
50 |
- tweet_text = Utils.value_at(event.payload, interpolated(event.payload)['message_path']) |
|
50 |
+ tweet_text = Utils.value_at(event.payload, interpolated(event)['message_path']) |
|
51 | 51 |
if event.agent.type == "Agents::TwitterUserAgent" |
52 | 52 |
tweet_text = unwrap_tco_urls(tweet_text, event.payload) |
53 | 53 |
end |
@@ -83,4 +83,4 @@ module Agents |
||
83 | 83 |
end |
84 | 84 |
|
85 | 85 |
end |
86 |
-end |
|
86 |
+end |
@@ -5,6 +5,7 @@ require 'json_serialized_field' |
||
5 | 5 |
# fields. |
6 | 6 |
class Event < ActiveRecord::Base |
7 | 7 |
include JSONSerializedField |
8 |
+ include LiquidDroppable |
|
8 | 9 |
|
9 | 10 |
attr_accessible :lat, :lng, :payload, :user_id, :user, :expires_at |
10 | 11 |
|
@@ -41,3 +42,26 @@ class Event < ActiveRecord::Base |
||
41 | 42 |
Agent.receive!(:only_receivers => propagate_ids) unless propagate_ids.empty? |
42 | 43 |
end |
43 | 44 |
end |
45 |
+ |
|
46 |
+class EventDrop |
|
47 |
+ def initialize(event, payload = event.payload) |
|
48 |
+ super(event) |
|
49 |
+ @payload = payload |
|
50 |
+ end |
|
51 |
+ |
|
52 |
+ def before_method(key) |
|
53 |
+ if @payload.key?(key) |
|
54 |
+ @payload[key] |
|
55 |
+ else |
|
56 |
+ case key |
|
57 |
+ when 'agent' |
|
58 |
+ @object.agent |
|
59 |
+ end |
|
60 |
+ end |
|
61 |
+ end |
|
62 |
+ |
|
63 |
+ def each(&block) |
|
64 |
+ return to_enum(__method__) unless block |
|
65 |
+ @payload.each(&block) |
|
66 |
+ end |
|
67 |
+end |
@@ -86,6 +86,7 @@ |
||
86 | 86 |
<div class="col-md-12"> |
87 | 87 |
<div class="form-group"> |
88 | 88 |
<%= f.label :options %> |
89 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="In this JSON hash, interpolation is available in almost all values using the Liquid templating language.<p>Available template variables include the following:<dl><dt><code>message</code>, <code>url</code>, etc.</dt><dd>Refers to the corresponding key's value of each incoming event's payload.</dd><dt><code>agent</code></dt><dd>Refers to the agent that created each incoming event. It has attributes like <code>type</code>, <code>name</code> and <code>options</code>, so <code>{{agent.type}}</code> will expand to <code>WebsiteAgent</code> if an incoming event is created by that agent.</dd></dl></p><p>To access user credentials, use the <code>credential</code> tag like this: <code>{% credential <em>bare_key_name</em> %}</code></p>"></span> |
|
89 | 90 |
<textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor <%= (@agent.new_record? && @agent.options == {}) ? "showing-default" : "" %>"> |
90 | 91 |
<%= Utils.jsonify((@agent.new_record? && @agent.options == {}) ? @agent.default_options : @agent.options) %> |
91 | 92 |
</textarea> |
@@ -116,6 +116,11 @@ |
||
116 | 116 |
None |
117 | 117 |
<% end %> |
118 | 118 |
</p> |
119 |
+ |
|
120 |
+ <p> |
|
121 |
+ <b>Propagate immediately:</b> |
|
122 |
+ <%= yes_no @agent.propagate_immediately %> |
|
123 |
+ </p> |
|
119 | 124 |
<% end %> |
120 | 125 |
|
121 | 126 |
<% if @agent.can_create_events? %> |
@@ -0,0 +1,160 @@ |
||
1 |
+#!/usr/bin/env ruby |
|
2 |
+require 'open3' |
|
3 |
+require 'io/console' |
|
4 |
+ |
|
5 |
+unless `which heroku` =~ /heroku/ |
|
6 |
+ puts "It looks like the heroku command line tool hasn't been installed yet. Please install" |
|
7 |
+ puts "the Heroku Toolbelt from https://toolbelt.heroku.com, run 'heroku auth:login', and then" |
|
8 |
+ puts "run this script again." |
|
9 |
+ exit 1 |
|
10 |
+end |
|
11 |
+ |
|
12 |
+def capture(cmd, opts = {}) |
|
13 |
+ o, s = Open3.capture2e(cmd, opts) |
|
14 |
+ o.strip |
|
15 |
+end |
|
16 |
+ |
|
17 |
+def ask(question, opts = {}) |
|
18 |
+ print question + " " |
|
19 |
+ STDOUT.flush |
|
20 |
+ (opts[:noecho] ? STDIN.noecho(&:gets) : gets).strip |
|
21 |
+end |
|
22 |
+ |
|
23 |
+def nag(question, opts = {}) |
|
24 |
+ answer = '' |
|
25 |
+ while answer.length == 0 |
|
26 |
+ answer = ask(question, opts) |
|
27 |
+ end |
|
28 |
+ answer |
|
29 |
+end |
|
30 |
+ |
|
31 |
+def yes?(question) |
|
32 |
+ ask(question + " (y/n)") =~ /^y/i |
|
33 |
+end |
|
34 |
+ |
|
35 |
+def grab_heroku_config! |
|
36 |
+ config_data = capture("heroku config -s") |
|
37 |
+ $config = {} |
|
38 |
+ if config_data !~ /has no config vars/ |
|
39 |
+ config_data.split("\n").map do |line| |
|
40 |
+ next if line =~ /^\s*(#|$)/ # skip comments and empty lines |
|
41 |
+ first_equal_sign = line.index('=') |
|
42 |
+ $config[line.slice(0, first_equal_sign)] = line.slice(first_equal_sign + 1, line.length) |
|
43 |
+ end |
|
44 |
+ end |
|
45 |
+end |
|
46 |
+ |
|
47 |
+def set_value(key, value, options = {}) |
|
48 |
+ if $config[key].nil? || $config[key] == '' || ($config[key] != value && options[:force] != false) |
|
49 |
+ puts "Setting #{key} to #{value}" unless options[:silent] |
|
50 |
+ puts capture("heroku config:set #{key}=#{value}") |
|
51 |
+ end |
|
52 |
+end |
|
53 |
+ |
|
54 |
+unless File.exists?(File.expand_path("~/.netrc")) && File.read(File.expand_path("~/.netrc")) =~ /heroku/ |
|
55 |
+ puts "It looks like you need to log in to Heroku. Please run 'heroku auth:login' before continuing." |
|
56 |
+ exit 1 |
|
57 |
+end |
|
58 |
+ |
|
59 |
+puts "Welcome #{`heroku auth:whoami`.strip}! It looks like you're logged into Heroku." |
|
60 |
+puts |
|
61 |
+ |
|
62 |
+info = capture("heroku info") |
|
63 |
+if info =~ /No app specified/i |
|
64 |
+ puts "It looks like you don't have a Heroku app set up yet for this repo." |
|
65 |
+ puts "You can either exit now and run 'heroku create', or I can do it for you." |
|
66 |
+ if yes?("Would you like me to create a Heroku app for you now in this repo?") |
|
67 |
+ puts `heroku create` |
|
68 |
+ info = capture("heroku info") |
|
69 |
+ else |
|
70 |
+ puts "Okay, exiting so you can do it." |
|
71 |
+ exit 0 |
|
72 |
+ end |
|
73 |
+end |
|
74 |
+ |
|
75 |
+app_name = info.scan(/http:\/\/([\w\d-]+)\.herokuapp\.com/).flatten.first |
|
76 |
+ |
|
77 |
+unless yes?("Your Heroku app name is #{app_name}. Is this correct?") |
|
78 |
+ puts "Well, then I'm not sure what to do here, sorry." |
|
79 |
+ exit 1 |
|
80 |
+end |
|
81 |
+ |
|
82 |
+grab_heroku_config! |
|
83 |
+ |
|
84 |
+if $config.length > 0 |
|
85 |
+ puts |
|
86 |
+ puts "Your current Heroku config:" |
|
87 |
+ $config.each do |key, value| |
|
88 |
+ puts ' ' + key + ' ' * (25 - [key.length, 25].min) + '= ' + value |
|
89 |
+ end |
|
90 |
+end |
|
91 |
+ |
|
92 |
+unless $config['APP_SECRET_TOKEN'] |
|
93 |
+ puts "Setting up APP_SECRET_TOKEN..." |
|
94 |
+ puts capture("heroku config:set APP_SECRET_TOKEN=`rake secret`") |
|
95 |
+end |
|
96 |
+ |
|
97 |
+set_value 'BUILDPACK_URL', "https://github.com/ddollar/heroku-buildpack-multi.git" |
|
98 |
+set_value 'PROCFILE_PATH', "deployment/heroku/Procfile.heroku", force: false |
|
99 |
+set_value 'ON_HEROKU', "true" |
|
100 |
+set_value 'FORCE_SSL', "true" |
|
101 |
+set_value 'DOMAIN', "#{app_name}.herokuapp.com", force: false |
|
102 |
+ |
|
103 |
+unless $config['INVITATION_CODE'] |
|
104 |
+ puts "You need to set an invitation code for your Huginn instance. If you plan to share this instance, you will" |
|
105 |
+ puts "tell this code to anyone who you'd like to invite. If you won't share it, then just set this to something" |
|
106 |
+ puts "that people will not guess." |
|
107 |
+ |
|
108 |
+ invitation_code = nag("What code would you like to use?") |
|
109 |
+ set_value 'INVITATION_CODE', invitation_code |
|
110 |
+end |
|
111 |
+ |
|
112 |
+unless $config['SMTP_DOMAIN'] && $config['SMTP_USER_NAME'] && $config['SMTP_PASSWORD'] && $config['SMTP_SERVER'] && $config['EMAIL_FROM_ADDRESS'] |
|
113 |
+ puts "Okay, let's setup outgoing email settings. The simplest solution is to use the free sendgrid Heroku addon." |
|
114 |
+ puts "If you'd like to use your own server, or your Gmail account, please see .env.example and set" |
|
115 |
+ puts "SMTP_DOMAIN, SMTP_USER_NAME, SMTP_PASSWORD, and SMTP_SERVER with 'heroku config:set'." |
|
116 |
+ if yes?("Should I enable the free sendgrid addon?") |
|
117 |
+ puts capture("heroku addons:add sendgrid") |
|
118 |
+ |
|
119 |
+ set_value 'SMTP_SERVER', "smtp.sendgrid.net", silent: true |
|
120 |
+ set_value 'SMTP_DOMAIN', "heroku.com", silent: true |
|
121 |
+ |
|
122 |
+ grab_heroku_config! |
|
123 |
+ set_value 'SMTP_USER_NAME', $config['SENDGRID_USERNAME'], silent: true |
|
124 |
+ set_value 'SMTP_PASSWORD', $config['SENDGRID_PASSWORD'], silent: true |
|
125 |
+ else |
|
126 |
+ puts "Okay, you'll need to set SMTP_DOMAIN, SMTP_USER_NAME, SMTP_PASSWORD, and SMTP_SERVER with 'heroku config:set' manually." |
|
127 |
+ end |
|
128 |
+ |
|
129 |
+ unless $config['EMAIL_FROM_ADDRESS'] |
|
130 |
+ email = nag("What email address would you like email to appear to be sent from?") |
|
131 |
+ set_value 'EMAIL_FROM_ADDRESS', email |
|
132 |
+ end |
|
133 |
+end |
|
134 |
+ |
|
135 |
+branch = capture("git rev-parse --abbrev-ref HEAD") |
|
136 |
+if yes?("Should I push your current branch (#{branch}) to heroku?") |
|
137 |
+ puts "This may take a moment..." |
|
138 |
+ puts capture("git push heroku #{branch}:master -f") |
|
139 |
+ |
|
140 |
+ puts "Running database migrations..." |
|
141 |
+ puts capture("heroku run rake db:migrate") |
|
142 |
+ |
|
143 |
+ puts |
|
144 |
+ puts |
|
145 |
+ puts "I can make an admin user on your new Huginn instance and setup some example Agents." |
|
146 |
+ if yes?("Should I create a new admin user and some example Agents?") |
|
147 |
+ seed_email = nag "Okay, what is your email address?" |
|
148 |
+ seed_username = nag "And what username would you like to login as?" |
|
149 |
+ seed_password = nag "Finally, what password would you like to use?", noecho: true |
|
150 |
+ puts "\nJust a moment..." |
|
151 |
+ |
|
152 |
+ capture("heroku run rake db:seed SEED_EMAIL=#{seed_email} SEED_USERNAME=#{seed_username} SEED_PASSWORD=#{seed_password}") |
|
153 |
+ puts |
|
154 |
+ puts |
|
155 |
+ puts "Okay, you should be all set! Visit https://#{app_name}.herokuapp.com and login as '#{seed_username}' with your password." |
|
156 |
+ end |
|
157 |
+end |
|
158 |
+ |
|
159 |
+puts |
|
160 |
+puts "Done!" |
@@ -5,5 +5,5 @@ Delayed::Worker.read_ahead = 5 |
||
5 | 5 |
Delayed::Worker.default_priority = 10 |
6 | 6 |
Delayed::Worker.delay_jobs = !Rails.env.test? |
7 | 7 |
|
8 |
-Delayed::Worker.logger = Logger.new(Rails.root.join('log', 'delayed_job.log')) |
|
9 |
-Delayed::Worker.logger.level = Logger::DEBUG |
|
8 |
+# Delayed::Worker.logger = Logger.new(Rails.root.join('log', 'delayed_job.log')) |
|
9 |
+# Delayed::Worker.logger.level = Logger::DEBUG |
@@ -0,0 +1,10 @@ |
||
1 |
+Rails::Rack::Logger.class_eval do |
|
2 |
+ def call_with_silence_worker_status(env) |
|
3 |
+ previous_level = Rails.logger.level |
|
4 |
+ Rails.logger.level = Logger::ERROR if env['PATH_INFO'] =~ %r{^/worker_status} |
|
5 |
+ call_without_silence_worker_status(env) |
|
6 |
+ ensure |
|
7 |
+ Rails.logger.level = previous_level |
|
8 |
+ end |
|
9 |
+ alias_method_chain :call, :silence_worker_status |
|
10 |
+end |
@@ -1,4 +1,29 @@ |
||
1 | 1 |
class MigrateAgentsToLiquidTemplating < ActiveRecord::Migration |
2 |
+ class Agent < ActiveRecord::Base |
|
3 |
+ include JSONSerializedField |
|
4 |
+ json_serialize :options, :memory |
|
5 |
+ end |
|
6 |
+ class Agents::HipchatAgent < Agent |
|
7 |
+ end |
|
8 |
+ class Agents::EventFormattingAgent < Agent |
|
9 |
+ end |
|
10 |
+ class Agents::PushbulletAgent < Agent |
|
11 |
+ end |
|
12 |
+ class Agents::JabberAgent < Agent |
|
13 |
+ end |
|
14 |
+ class Agents::DataOutputAgent < Agent |
|
15 |
+ end |
|
16 |
+ class Agents::TranslationAgent < Agent |
|
17 |
+ end |
|
18 |
+ class Agents::TwitterPublishAgent < Agent |
|
19 |
+ end |
|
20 |
+ class Agents::TriggerAgent < Agent |
|
21 |
+ end |
|
22 |
+ class Agents::PeakDetectorAgent < Agent |
|
23 |
+ end |
|
24 |
+ class Agents::HumanTaskAgent < Agent |
|
25 |
+ end |
|
26 |
+ |
|
2 | 27 |
def up |
3 | 28 |
Agent.where(:type => 'Agents::HipchatAgent').each do |agent| |
4 | 29 |
LiquidMigrator.convert_all_agent_options(agent) |
@@ -0,0 +1,21 @@ |
||
1 |
+class ConvertEfaSkipAgent < ActiveRecord::Migration |
|
2 |
+ def up |
|
3 |
+ Agent.where(type: 'Agents::EventFormattingAgent').each do |agent| |
|
4 |
+ agent.options_will_change! |
|
5 |
+ unless agent.options.delete('skip_agent').to_s == 'true' |
|
6 |
+ agent.options['instructions'] = { |
|
7 |
+ 'agent' => '{{agent.type}}' |
|
8 |
+ }.update(agent.options['instructions'] || {}) |
|
9 |
+ end |
|
10 |
+ agent.save! |
|
11 |
+ end |
|
12 |
+ end |
|
13 |
+ |
|
14 |
+ def down |
|
15 |
+ Agent.where(type: 'Agents::EventFormattingAgent').each do |agent| |
|
16 |
+ agent.options_will_change! |
|
17 |
+ agent.options['skip_agent'] = (agent.options['instructions'] || {})['agent'] == '{{agent.type}}' |
|
18 |
+ agent.save! |
|
19 |
+ end |
|
20 |
+ end |
|
21 |
+end |
@@ -0,0 +1,30 @@ |
||
1 |
+class AdoptXpathInWebsiteAgent < ActiveRecord::Migration |
|
2 |
+ class Agent < ActiveRecord::Base |
|
3 |
+ include JSONSerializedField |
|
4 |
+ json_serialize :options |
|
5 |
+ end |
|
6 |
+ |
|
7 |
+ def up |
|
8 |
+ Agent.where(type: 'Agents::WebsiteAgent').each do |agent| |
|
9 |
+ extract = agent.options['extract'] |
|
10 |
+ next unless extract.is_a?(Hash) && extract.all? { |name, detail| |
|
11 |
+ detail.key?('xpath') || detail.key?('css') |
|
12 |
+ } |
|
13 |
+ |
|
14 |
+ agent.options_will_change! |
|
15 |
+ agent.options['extract'].each { |name, extraction| |
|
16 |
+ case |
|
17 |
+ when extraction.delete('text') |
|
18 |
+ extraction['value'] = './/text()' |
|
19 |
+ when attr = extraction.delete('attr') |
|
20 |
+ extraction['value'] = "@#{attr}" |
|
21 |
+ end |
|
22 |
+ } |
|
23 |
+ agent.save! |
|
24 |
+ end |
|
25 |
+ end |
|
26 |
+ |
|
27 |
+ def down |
|
28 |
+ raise ActiveRecord::IrreversibleMigration, "Cannot revert this migration" |
|
29 |
+ end |
|
30 |
+end |
@@ -1,10 +1,10 @@ |
||
1 | 1 |
# This file should contain all the record creation needed to seed the database with its default values. |
2 | 2 |
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). |
3 | 3 |
|
4 |
-user = User.find_or_initialize_by(:email => "admin@example.com") |
|
5 |
-user.username = "admin" |
|
6 |
-user.password = "password" |
|
7 |
-user.password_confirmation = "password" |
|
4 |
+user = User.find_or_initialize_by(:email => ENV['SEED_EMAIL'] || "admin@example.com") |
|
5 |
+user.username = ENV['SEED_USERNAME'] || "admin" |
|
6 |
+user.password = ENV['SEED_PASSWORD'] || "password" |
|
7 |
+user.password_confirmation = ENV['SEED_PASSWORD'] || "password" |
|
8 | 8 |
user.invitation_code = User::INVITATION_CODES.first |
9 | 9 |
user.admin = true |
10 | 10 |
user.save! |
@@ -31,9 +31,9 @@ unless user.agents.where(:name => "XKCD Source").exists? |
||
31 | 31 |
'mode' => "on_change", |
32 | 32 |
'expected_update_period_in_days' => 5, |
33 | 33 |
'extract' => { |
34 |
- 'url' => { 'css' => "#comic img", 'attr' => "src" }, |
|
35 |
- 'title' => { 'css' => "#comic img", 'attr' => "alt" }, |
|
36 |
- 'hovertext' => { 'css' => "#comic img", 'attr' => "title" } |
|
34 |
+ 'url' => { 'css' => "#comic img", 'value' => "@src" }, |
|
35 |
+ 'title' => { 'css' => "#comic img", 'value' => "@alt" }, |
|
36 |
+ 'hovertext' => { 'css' => "#comic img", 'value' => "@title" } |
|
37 | 37 |
} |
38 | 38 |
}).save! |
39 | 39 |
end |
@@ -47,8 +47,8 @@ unless user.agents.where(:name => "iTunes Trailer Source").exists? |
||
47 | 47 |
'type' => "xml", |
48 | 48 |
'expected_update_period_in_days' => 5, |
49 | 49 |
'extract' => { |
50 |
- 'title' => { 'css' => "item title", 'text' => true}, |
|
51 |
- 'url' => { 'css' => "item link", 'text' => true} |
|
50 |
+ 'title' => { 'css' => "item title", 'value' => ".//text()"}, |
|
51 |
+ 'url' => { 'css' => "item link", 'value' => ".//text()"} |
|
52 | 52 |
} |
53 | 53 |
}).save! |
54 | 54 |
end |
@@ -0,0 +1,4 @@ |
||
1 |
+# This Procfile is intended for Heroku, and is detected by the Gemfile. DO NOT REMOVE THIS LINE! |
|
2 |
+ |
|
3 |
+# deployment/heroku/unicorn.rb is a special Unicorn config file that also spawns workers. |
|
4 |
+web: bundle exec unicorn -p $PORT -c ./deployment/heroku/unicorn.rb |
@@ -0,0 +1,51 @@ |
||
1 |
+require "net/http" |
|
2 |
+ |
|
3 |
+worker_processes Integer(ENV["WEB_CONCURRENCY"] || 2) |
|
4 |
+timeout 15 |
|
5 |
+preload_app true |
|
6 |
+ |
|
7 |
+# Note that this will only work correctly when running Heroku with ONE web worker. |
|
8 |
+# If you want to run more than one, use the standard Huginn Procfile instead, with separate web and job entries. |
|
9 |
+# You'll need to set the Heroku config variable PROCFILE_PATH to 'Procfile'. |
|
10 |
+Thread.new do |
|
11 |
+ worker_pid = nil |
|
12 |
+ while true |
|
13 |
+ if worker_pid.nil? |
|
14 |
+ worker_pid = spawn("bundle exec rails runner bin/threaded.rb") |
|
15 |
+ puts "New threaded worker PID: #{worker_pid}" |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ sleep 45 |
|
19 |
+ |
|
20 |
+ if ENV['DOMAIN'] |
|
21 |
+ force_ssl = ENV['FORCE_SSL'].present? && ENV['FORCE_SSL'] == 'true' |
|
22 |
+ Net::HTTP.get_response(URI((force_ssl ? "https://" : "http://") + ENV['DOMAIN'])) |
|
23 |
+ end |
|
24 |
+ |
|
25 |
+ begin |
|
26 |
+ Process.getpgid worker_pid |
|
27 |
+ rescue Errno::ESRCH |
|
28 |
+ # No longer running |
|
29 |
+ worker_pid = nil |
|
30 |
+ end |
|
31 |
+ end |
|
32 |
+end |
|
33 |
+ |
|
34 |
+before_fork do |server, worker| |
|
35 |
+ Signal.trap 'TERM' do |
|
36 |
+ puts 'Unicorn master intercepting TERM and sending myself QUIT instead' |
|
37 |
+ Process.kill 'QUIT', Process.pid |
|
38 |
+ end |
|
39 |
+ |
|
40 |
+ defined?(ActiveRecord::Base) and |
|
41 |
+ ActiveRecord::Base.connection.disconnect! |
|
42 |
+end |
|
43 |
+ |
|
44 |
+after_fork do |server, worker| |
|
45 |
+ Signal.trap 'TERM' do |
|
46 |
+ puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT' |
|
47 |
+ end |
|
48 |
+ |
|
49 |
+ defined?(ActiveRecord::Base) and |
|
50 |
+ ActiveRecord::Base.establish_connection |
|
51 |
+end |
@@ -0,0 +1,67 @@ |
||
1 |
+require "google/api_client" |
|
2 |
+ |
|
3 |
+class GoogleCalendar |
|
4 |
+ |
|
5 |
+ def initialize(config, logger) |
|
6 |
+ @config = config |
|
7 |
+ @key = Google::APIClient::PKCS12.load_key(@config['google']['key_file'], @config['google']['key_secret']) |
|
8 |
+ @client = Google::APIClient.new(application_name: "Huginn", application_version: "0.0.1") |
|
9 |
+ @client.retries = 2 |
|
10 |
+ @logger ||= logger |
|
11 |
+ |
|
12 |
+ @calendar = @client.discovered_api('calendar','v3') |
|
13 |
+ |
|
14 |
+ @logger.info("Setup") |
|
15 |
+ @logger.debug @calendar.inspect |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ def auth_as |
|
19 |
+ @client.authorization = Signet::OAuth2::Client.new({ |
|
20 |
+ token_credential_uri: 'https://accounts.google.com/o/oauth2/token', |
|
21 |
+ audience: 'https://accounts.google.com/o/oauth2/token', |
|
22 |
+ scope: 'https://www.googleapis.com/auth/calendar', |
|
23 |
+ issuer: @config['google']['service_account_email'], |
|
24 |
+ signing_key: @key |
|
25 |
+ }); |
|
26 |
+ |
|
27 |
+ @client.authorization.fetch_access_token! |
|
28 |
+ end |
|
29 |
+ |
|
30 |
+ # who - String: email of user to add event |
|
31 |
+ # details - JSON String: see https://developers.google.com/google-apps/calendar/v3/reference/events/insert |
|
32 |
+ def publish_as(who, details) |
|
33 |
+ auth_as |
|
34 |
+ |
|
35 |
+ @logger.info("Attempting to create event for " + who) |
|
36 |
+ @logger.debug details.to_yaml |
|
37 |
+ |
|
38 |
+ ret = @client.execute( |
|
39 |
+ api_method: @calendar.events.insert, |
|
40 |
+ parameters: {'calendarId' => who, 'sendNotifications' => true}, |
|
41 |
+ body: details.to_json, |
|
42 |
+ headers: {'Content-Type' => 'application/json'} |
|
43 |
+ ) |
|
44 |
+ @logger.debug ret.to_yaml |
|
45 |
+ ret |
|
46 |
+ end |
|
47 |
+ |
|
48 |
+ def events_as(who, date) |
|
49 |
+ auth_as |
|
50 |
+ |
|
51 |
+ date ||= Date.today |
|
52 |
+ |
|
53 |
+ @logger.info("Attempting to receive events for "+who) |
|
54 |
+ @logger.debug details.to_yaml |
|
55 |
+ |
|
56 |
+ ret = @client.execute( |
|
57 |
+ api_method: @calendar.events.list, |
|
58 |
+ parameters: {'calendarId' => who, 'sendNotifications' => true}, |
|
59 |
+ body: details.to_json, |
|
60 |
+ headers: {'Content-Type' => 'application/json'} |
|
61 |
+ ) |
|
62 |
+ |
|
63 |
+ @logger.debug ret.to_yaml |
|
64 |
+ ret |
|
65 |
+ end |
|
66 |
+ |
|
67 |
+end |
@@ -0,0 +1,435 @@ |
||
1 |
+--- |
|
2 |
+http_interactions: |
|
3 |
+- request: |
|
4 |
+ method: get |
|
5 |
+ uri: https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest |
|
6 |
+ body: |
|
7 |
+ encoding: UTF-8 |
|
8 |
+ string: '' |
|
9 |
+ headers: |
|
10 |
+ User-Agent: |
|
11 |
+ - |- |
|
12 |
+ Huginn/0.0.1 google-api-ruby-client/0.7.1 Linux/3.13.0-29-generic |
|
13 |
+ (gzip) |
|
14 |
+ Accept-Encoding: |
|
15 |
+ - gzip |
|
16 |
+ Content-Type: |
|
17 |
+ - '' |
|
18 |
+ Accept: |
|
19 |
+ - "*/*" |
|
20 |
+ response: |
|
21 |
+ status: |
|
22 |
+ code: 200 |
|
23 |
+ message: OK |
|
24 |
+ headers: |
|
25 |
+ Expires: |
|
26 |
+ - Sat, 28 Jun 2014 17:21:12 GMT |
|
27 |
+ Date: |
|
28 |
+ - Sat, 28 Jun 2014 17:16:12 GMT |
|
29 |
+ Etag: |
|
30 |
+ - '"C11OM5Qtr9122-scy_WeqND9D3o/icy_kevyvyjgCKjN6s1gb_9TUZs"' |
|
31 |
+ Content-Type: |
|
32 |
+ - application/json; charset=UTF-8 |
|
33 |
+ Content-Encoding: |
|
34 |
+ - gzip |
|
35 |
+ X-Content-Type-Options: |
|
36 |
+ - nosniff |
|
37 |
+ X-Frame-Options: |
|
38 |
+ - SAMEORIGIN |
|
39 |
+ X-Xss-Protection: |
|
40 |
+ - 1; mode=block |
|
41 |
+ Content-Length: |
|
42 |
+ - '11266' |
|
43 |
+ Server: |
|
44 |
+ - GSE |
|
45 |
+ Age: |
|
46 |
+ - '195' |
|
47 |
+ Cache-Control: |
|
48 |
+ - public, max-age=300, must-revalidate, no-transform |
|
49 |
+ Alternate-Protocol: |
|
50 |
+ - 443:quic |
|
51 |
+ body: |
|
52 |
+ encoding: ASCII-8BIT |
|
53 |
+ string: !binary |- |
|
54 |
+ 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 |
|
304 |
+ 4Rn8/2/P/j8y14+toGwBAA== |
|
305 |
+ http_version: |
|
306 |
+ recorded_at: Sat, 28 Jun 2014 17:19:27 GMT |
|
307 |
+- request: |
|
308 |
+ method: post |
|
309 |
+ uri: https://accounts.google.com/o/oauth2/token |
|
310 |
+ body: |
|
311 |
+ encoding: ASCII-8BIT |
|
312 |
+ string: grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiIxMDI5OTM2OTY2MzI2LW5jamQ3Nzc2cGNzcGM5OGhzZzgyZ3NiNTZ0MzIxN2VmQGRldmVsb3Blci5nc2VydmljZWFjY291bnQuY29tIiwic2NvcGUiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9hdXRoL2NhbGVuZGFyIiwiYXVkIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL3Rva2VuIiwiZXhwIjoxNDAzOTc2MDI3LCJpYXQiOjE0MDM5NzU5MDd9.G2t_IqQofzzXKJAySGu4MulAybOp3BjptAk_tra7-ALy2pWu0jKw8XblP4T0YPgyMcbhcSJ_OhYl7Inmkxc3xhWsN-ZQDtacIyLv9roIRbvm5zCFKceJRISu2nZuIUTGTeoVuotPh6KRRvXIk1RW_Rc7L0eLeGTWn1USqdNwlfU |
|
313 |
+ headers: |
|
314 |
+ Cache-Control: |
|
315 |
+ - no-store |
|
316 |
+ Content-Type: |
|
317 |
+ - application/x-www-form-urlencoded |
|
318 |
+ Accept-Encoding: |
|
319 |
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 |
|
320 |
+ Accept: |
|
321 |
+ - "*/*" |
|
322 |
+ User-Agent: |
|
323 |
+ - Ruby |
|
324 |
+ response: |
|
325 |
+ status: |
|
326 |
+ code: 200 |
|
327 |
+ message: OK |
|
328 |
+ headers: |
|
329 |
+ Content-Type: |
|
330 |
+ - application/json; charset=utf-8 |
|
331 |
+ Cache-Control: |
|
332 |
+ - no-cache, no-store, max-age=0, must-revalidate |
|
333 |
+ Pragma: |
|
334 |
+ - no-cache |
|
335 |
+ Expires: |
|
336 |
+ - Fri, 01 Jan 1990 00:00:00 GMT |
|
337 |
+ Date: |
|
338 |
+ - Sat, 28 Jun 2014 17:19:28 GMT |
|
339 |
+ Content-Disposition: |
|
340 |
+ - attachment; filename="json.txt"; filename*=UTF-8''json.txt |
|
341 |
+ X-Content-Type-Options: |
|
342 |
+ - nosniff |
|
343 |
+ X-Frame-Options: |
|
344 |
+ - SAMEORIGIN |
|
345 |
+ X-Xss-Protection: |
|
346 |
+ - 1; mode=block |
|
347 |
+ Server: |
|
348 |
+ - GSE |
|
349 |
+ Alternate-Protocol: |
|
350 |
+ - 443:quic |
|
351 |
+ Transfer-Encoding: |
|
352 |
+ - chunked |
|
353 |
+ body: |
|
354 |
+ encoding: UTF-8 |
|
355 |
+ string: |- |
|
356 |
+ { |
|
357 |
+ "access_token" : "ya29.MgBhhNLHjzGOZBoAAADoyvaDO5G6GHmaxIYqa5EGZ5t8kL-unXKywIRYoYgQxw", |
|
358 |
+ "token_type" : "Bearer", |
|
359 |
+ "expires_in" : 3600 |
|
360 |
+ } |
|
361 |
+ http_version: |
|
362 |
+ recorded_at: Sat, 28 Jun 2014 17:19:28 GMT |
|
363 |
+- request: |
|
364 |
+ method: post |
|
365 |
+ uri: https://www.googleapis.com/calendar/v3/calendars/sqv39gj35tc837gdns1g4d81cg@group.calendar.google.com/events?sendNotifications=true |
|
366 |
+ body: |
|
367 |
+ encoding: UTF-8 |
|
368 |
+ string: '{"visibility":"default","summary":"Awesome event","description":"An |
|
369 |
+ example event with text. Pro tip: DateTimes are in RFC3339","end":{"dateTime":"2014-10-02T11:00:00-05:00"},"start":{"dateTime":"2014-10-02T10:00:00-05:00"}}' |
|
370 |
+ headers: |
|
371 |
+ User-Agent: |
|
372 |
+ - |- |
|
373 |
+ Huginn/0.0.1 google-api-ruby-client/0.7.1 Linux/3.13.0-29-generic |
|
374 |
+ (gzip) |
|
375 |
+ Content-Type: |
|
376 |
+ - application/json |
|
377 |
+ Accept-Encoding: |
|
378 |
+ - gzip |
|
379 |
+ Authorization: |
|
380 |
+ - Bearer ya29.MgBhhNLHjzGOZBoAAADoyvaDO5G6GHmaxIYqa5EGZ5t8kL-unXKywIRYoYgQxw |
|
381 |
+ Cache-Control: |
|
382 |
+ - no-store |
|
383 |
+ Accept: |
|
384 |
+ - "*/*" |
|
385 |
+ response: |
|
386 |
+ status: |
|
387 |
+ code: 200 |
|
388 |
+ message: OK |
|
389 |
+ headers: |
|
390 |
+ Cache-Control: |
|
391 |
+ - no-cache, no-store, max-age=0, must-revalidate |
|
392 |
+ Pragma: |
|
393 |
+ - no-cache |
|
394 |
+ Expires: |
|
395 |
+ - Fri, 01 Jan 1990 00:00:00 GMT |
|
396 |
+ Date: |
|
397 |
+ - Sat, 28 Jun 2014 17:19:31 GMT |
|
398 |
+ Etag: |
|
399 |
+ - '"BZUCgRsJHN1b3Y4VmmLXiJzEzGI/MjgwNzk1MTk0MTk3MjAwMA"' |
|
400 |
+ Content-Type: |
|
401 |
+ - application/json; charset=UTF-8 |
|
402 |
+ Content-Encoding: |
|
403 |
+ - gzip |
|
404 |
+ X-Content-Type-Options: |
|
405 |
+ - nosniff |
|
406 |
+ X-Frame-Options: |
|
407 |
+ - SAMEORIGIN |
|
408 |
+ X-Xss-Protection: |
|
409 |
+ - 1; mode=block |
|
410 |
+ Server: |
|
411 |
+ - GSE |
|
412 |
+ Alternate-Protocol: |
|
413 |
+ - 443:quic |
|
414 |
+ Transfer-Encoding: |
|
415 |
+ - chunked |
|
416 |
+ body: |
|
417 |
+ encoding: ASCII-8BIT |
|
418 |
+ string: !binary |- |
|
419 |
+ H4sIAAAAAAAAAH1T0U7bMBR971dY3ePW1IlLmlSaRkdXRkWqCZUxKl6Mfeu6 |
|
420 |
+ iZ1gOw0E8e9LAh2btvFgKc49Pufce+zHHuqnUvP+BPUZzUBzat7BHrTrf2hK |
|
421 |
+ 4KhoSzf9z+vLE3FhF1+X/i25Hn1X6vyHXNRf6tOzYbIT1bJO/WSV4maRZDet |
|
422 |
+ kulNv6OQHbfd850RocF7mpa3dp9FjJfW5zLtQNZRV9rORK430ijg3f+tU9m5 |
|
423 |
+ 1Glb2TpX2MlwWFWVJ/JcZOCxXA0Proed608g+UdG1inVC73UZxVXc8OvFjVX |
|
424 |
+ 1eg6uPBZPU/plRWMzIOkzjTdJT4/TUZJzfX69KhOrhhez8T9dcCn66ozwQxQ |
|
425 |
+ B10bAfZHAxwOgmjljyd+PAliD2O87nBlwf+LI9iLo/AZZ0ulqHlocdMKbK4A |
|
426 |
+ vU6cg2VGFk7mugNoBPdUFdkLBlXSbZGDe+ehbyZHThYTNGtkV1KBRdQAkhpd |
|
427 |
+ zE8IIfGr+9w0ZI891CSqqMxaZh8HcUzCOAxJEA402/HxeBwWzBYsjrZWRIGw |
|
428 |
+ t0ehI4E/hs0xb+SzvADjCQtmLxlQxvJSuzaEfg89tVq5EVTLGv5Ws3d7Eosd |
|
429 |
+ OXIsImPBtfXFiEc+E8fC5GXhHWL8LdrWfjMQaYuMPiypgpboaymk1ug8Z7Sd |
|
430 |
+ UfMhnnEWsk0DcKaEFzfNpTLu4IS/zOhXOj4eYLLCuIlmgvF73GZ06AO6B/Hm |
|
431 |
+ Of9f5+QJzS7PZm9f+OM/O2yM35WgWauA270B1TxIMPZgobQwgw0tM/faX++p |
|
432 |
+ 9xPxx2cruwMAAA== |
|
433 |
+ http_version: |
|
434 |
+ recorded_at: Sat, 28 Jun 2014 17:19:31 GMT |
|
435 |
+recorded_with: VCR 2.9.2 |
@@ -0,0 +1,356 @@ |
||
1 |
+<?xml version="1.0" encoding="UTF-8"?> |
|
2 |
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US"> |
|
3 |
+ <id>tag:github.com,2008:/cantino/huginn/commits/master</id> |
|
4 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commits/master"/> |
|
5 |
+ <link type="application/atom+xml" rel="self" href="https://github.com/cantino/huginn/commits/master.atom"/> |
|
6 |
+ <title>Recent Commits to huginn:master</title> |
|
7 |
+ <updated>2014-07-16T22:26:22-07:00</updated> |
|
8 |
+ <entry> |
|
9 |
+ <id>tag:github.com,2008:Grit::Commit/d0a844662846cf3c83b94c637c1803f03db5a5b0</id> |
|
10 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/d0a844662846cf3c83b94c637c1803f03db5a5b0"/> |
|
11 |
+ <title> |
|
12 |
+ Merge pull request #402 from albertsun/safer-liquid-migration |
|
13 |
+ </title> |
|
14 |
+ <updated>2014-07-16T22:26:22-07:00</updated> |
|
15 |
+ <media:thumbnail height="30" width="30" url="https://avatars3.githubusercontent.com/u/83835?s=30"/> |
|
16 |
+ <author> |
|
17 |
+ <name>cantino</name> |
|
18 |
+ <uri>https://github.com/cantino</uri> |
|
19 |
+ </author> |
|
20 |
+ <content type="html"> |
|
21 |
+ <pre style='white-space:pre-wrap;width:81ex'>Merge pull request #402 from albertsun/safer-liquid-migration |
|
22 |
+ |
|
23 |
+Inline models into migration</pre> |
|
24 |
+ </content> |
|
25 |
+ </entry> |
|
26 |
+ <entry> |
|
27 |
+ <id>tag:github.com,2008:Grit::Commit/4a433806eeace44f1e39f02ac61cefdadf3597e2</id> |
|
28 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/4a433806eeace44f1e39f02ac61cefdadf3597e2"/> |
|
29 |
+ <title> |
|
30 |
+ inline models into migration |
|
31 |
+ </title> |
|
32 |
+ <updated>2014-07-16T15:25:08-04:00</updated> |
|
33 |
+ <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/382862?s=30"/> |
|
34 |
+ <author> |
|
35 |
+ <name>albertsun</name> |
|
36 |
+ <uri>https://github.com/albertsun</uri> |
|
37 |
+ </author> |
|
38 |
+ <content type="html"> |
|
39 |
+ <pre style='white-space:pre-wrap;width:81ex'>inline models into migration</pre> |
|
40 |
+ </content> |
|
41 |
+ </entry> |
|
42 |
+ <entry> |
|
43 |
+ <id>tag:github.com,2008:Grit::Commit/6ffa528ab0af7f9f5bb4b68437e7613e74fdb8c4</id> |
|
44 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/6ffa528ab0af7f9f5bb4b68437e7613e74fdb8c4"/> |
|
45 |
+ <title> |
|
46 |
+ Merge pull request #398 from knu/imap_use_uid |
|
47 |
+ </title> |
|
48 |
+ <updated>2014-07-15T19:47:37-07:00</updated> |
|
49 |
+ <media:thumbnail height="30" width="30" url="https://avatars3.githubusercontent.com/u/83835?s=30"/> |
|
50 |
+ <author> |
|
51 |
+ <name>cantino</name> |
|
52 |
+ <uri>https://github.com/cantino</uri> |
|
53 |
+ </author> |
|
54 |
+ <content type="html"> |
|
55 |
+ <pre style='white-space:pre-wrap;width:81ex'>Merge pull request #398 from knu/imap_use_uid |
|
56 |
+ |
|
57 |
+Use "last seen UID" in ImapFolderAgent</pre> |
|
58 |
+ </content> |
|
59 |
+ </entry> |
|
60 |
+ <entry> |
|
61 |
+ <id>tag:github.com,2008:Grit::Commit/c7e29492c98652cc9738c374d02dcbb7c9bdeac6</id> |
|
62 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/c7e29492c98652cc9738c374d02dcbb7c9bdeac6"/> |
|
63 |
+ <title> |
|
64 |
+ Merge pull request #391 from theofpa/master |
|
65 |
+ </title> |
|
66 |
+ <updated>2014-07-12T15:19:56-07:00</updated> |
|
67 |
+ <media:thumbnail height="30" width="30" url="https://avatars3.githubusercontent.com/u/83835?s=30"/> |
|
68 |
+ <author> |
|
69 |
+ <name>cantino</name> |
|
70 |
+ <uri>https://github.com/cantino</uri> |
|
71 |
+ </author> |
|
72 |
+ <content type="html"> |
|
73 |
+ <pre style='white-space:pre-wrap;width:81ex'>Merge pull request #391 from theofpa/master |
|
74 |
+ |
|
75 |
+Ignore xmlns when evaluating xpath</pre> |
|
76 |
+ </content> |
|
77 |
+ </entry> |
|
78 |
+ <entry> |
|
79 |
+ <id>tag:github.com,2008:Grit::Commit/f3552ece2e9af187bd5e613783dd27810b63c32f</id> |
|
80 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/f3552ece2e9af187bd5e613783dd27810b63c32f"/> |
|
81 |
+ <title> |
|
82 |
+ ImapFolderAgent: Emit a log message when creating an event or skipping it. |
|
83 |
+ </title> |
|
84 |
+ <updated>2014-07-11T19:19:12+09:00</updated> |
|
85 |
+ <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/10236?s=30"/> |
|
86 |
+ <author> |
|
87 |
+ <name>knu</name> |
|
88 |
+ <uri>https://github.com/knu</uri> |
|
89 |
+ </author> |
|
90 |
+ <content type="html"> |
|
91 |
+ <pre style='white-space:pre-wrap;width:81ex'>ImapFolderAgent: Emit a log message when creating an event or skipping it.</pre> |
|
92 |
+ </content> |
|
93 |
+ </entry> |
|
94 |
+ <entry> |
|
95 |
+ <id>tag:github.com,2008:Grit::Commit/d144d3797d2db362943357c6d85238ec657cfa06</id> |
|
96 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/d144d3797d2db362943357c6d85238ec657cfa06"/> |
|
97 |
+ <title> |
|
98 |
+ ImapFolderAgent: Enable notification of mails already marked as read. |
|
99 |
+ </title> |
|
100 |
+ <updated>2014-07-11T19:08:55+09:00</updated> |
|
101 |
+ <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/10236?s=30"/> |
|
102 |
+ <author> |
|
103 |
+ <name>knu</name> |
|
104 |
+ <uri>https://github.com/knu</uri> |
|
105 |
+ </author> |
|
106 |
+ <content type="html"> |
|
107 |
+ <pre style='white-space:pre-wrap;width:81ex'>ImapFolderAgent: Enable notification of mails already marked as read. |
|
108 |
+ |
|
109 |
+Add a condition key "is_unread" to allow user to select mails based on |
|
110 |
+the read status.</pre> |
|
111 |
+ </content> |
|
112 |
+ </entry> |
|
113 |
+ <entry> |
|
114 |
+ <id>tag:github.com,2008:Grit::Commit/d1196a35ada22418bf0cf8b0d5947c2164e983e6</id> |
|
115 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/d1196a35ada22418bf0cf8b0d5947c2164e983e6"/> |
|
116 |
+ <title> |
|
117 |
+ ImapFolderAgent: "conditions" must not actually be nil. |
|
118 |
+ </title> |
|
119 |
+ <updated>2014-07-11T18:02:09+09:00</updated> |
|
120 |
+ <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/10236?s=30"/> |
|
121 |
+ <author> |
|
122 |
+ <name>knu</name> |
|
123 |
+ <uri>https://github.com/knu</uri> |
|
124 |
+ </author> |
|
125 |
+ <content type="html"> |
|
126 |
+ <pre style='white-space:pre-wrap;width:81ex'>ImapFolderAgent: "conditions" must not actually be nil.</pre> |
|
127 |
+ </content> |
|
128 |
+ </entry> |
|
129 |
+ <entry> |
|
130 |
+ <id>tag:github.com,2008:Grit::Commit/280c09415ea8114d8a128cd7c2583ae0e0aa480d</id> |
|
131 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/280c09415ea8114d8a128cd7c2583ae0e0aa480d"/> |
|
132 |
+ <title> |
|
133 |
+ ImapFolderAgent: Do not fail when port is blank. |
|
134 |
+ </title> |
|
135 |
+ <updated>2014-07-11T18:02:09+09:00</updated> |
|
136 |
+ <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/10236?s=30"/> |
|
137 |
+ <author> |
|
138 |
+ <name>knu</name> |
|
139 |
+ <uri>https://github.com/knu</uri> |
|
140 |
+ </author> |
|
141 |
+ <content type="html"> |
|
142 |
+ <pre style='white-space:pre-wrap;width:81ex'>ImapFolderAgent: Do not fail when port is blank.</pre> |
|
143 |
+ </content> |
|
144 |
+ </entry> |
|
145 |
+ <entry> |
|
146 |
+ <id>tag:github.com,2008:Grit::Commit/045fb957b2370d80190fa8dc036863076d8806fb</id> |
|
147 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/045fb957b2370d80190fa8dc036863076d8806fb"/> |
|
148 |
+ <title> |
|
149 |
+ ImapFolderAgent now recognizes "true"/"false" as boolean option values. |
|
150 |
+ </title> |
|
151 |
+ <updated>2014-07-11T18:02:09+09:00</updated> |
|
152 |
+ <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/10236?s=30"/> |
|
153 |
+ <author> |
|
154 |
+ <name>knu</name> |
|
155 |
+ <uri>https://github.com/knu</uri> |
|
156 |
+ </author> |
|
157 |
+ <content type="html"> |
|
158 |
+ <pre style='white-space:pre-wrap;width:81ex'>ImapFolderAgent now recognizes "true"/"false" as boolean option values. |
|
159 |
+ |
|
160 |
+Add a utility method Agent#boolify to make it easier to handle boolean |
|
161 |
+option values.</pre> |
|
162 |
+ </content> |
|
163 |
+ </entry> |
|
164 |
+ <entry> |
|
165 |
+ <id>tag:github.com,2008:Grit::Commit/c1b9caa8ccb0c8b8f6103fc80b90fba57a822435</id> |
|
166 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/c1b9caa8ccb0c8b8f6103fc80b90fba57a822435"/> |
|
167 |
+ <title> |
|
168 |
+ ImapFolderAgent: Unstringify integer keys of a hash saved in JSON. |
|
169 |
+ </title> |
|
170 |
+ <updated>2014-07-11T18:01:26+09:00</updated> |
|
171 |
+ <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/10236?s=30"/> |
|
172 |
+ <author> |
|
173 |
+ <name>knu</name> |
|
174 |
+ <uri>https://github.com/knu</uri> |
|
175 |
+ </author> |
|
176 |
+ <content type="html"> |
|
177 |
+ <pre style='white-space:pre-wrap;width:81ex'>ImapFolderAgent: Unstringify integer keys of a hash saved in JSON.</pre> |
|
178 |
+ </content> |
|
179 |
+ </entry> |
|
180 |
+ <entry> |
|
181 |
+ <id>tag:github.com,2008:Grit::Commit/6a06a32447721abc4477979610e36db0650e2f92</id> |
|
182 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/6a06a32447721abc4477979610e36db0650e2f92"/> |
|
183 |
+ <title> |
|
184 |
+ ImapFolderAgent: Only keep a single UID value for each folder in memory. |
|
185 |
+ </title> |
|
186 |
+ <updated>2014-07-11T18:01:26+09:00</updated> |
|
187 |
+ <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/10236?s=30"/> |
|
188 |
+ <author> |
|
189 |
+ <name>knu</name> |
|
190 |
+ <uri>https://github.com/knu</uri> |
|
191 |
+ </author> |
|
192 |
+ <content type="html"> |
|
193 |
+ <pre style='white-space:pre-wrap;width:81ex'>ImapFolderAgent: Only keep a single UID value for each folder in memory. |
|
194 |
+ |
|
195 |
+Previously it used to keep a list of the UIDs of unread mails. Now we |
|
196 |
+start to assume that UIDs in a folder identified by a UID VALIDITY value |
|
197 |
+are strictly ascending (monotonically increasing) as suggested by RFC |
|
198 |
+3501 and 4549 and just keep the highest UID seen in the last run. |
|
199 |
+ |
|
200 |
+This enhancement will help reduce the size of memory typically where |
|
201 |
+mails are left unread forever.</pre> |
|
202 |
+ </content> |
|
203 |
+ </entry> |
|
204 |
+ <entry> |
|
205 |
+ <id>tag:github.com,2008:Grit::Commit/9ed63e45b247c30a02e8e59b4d24fccbe8644876</id> |
|
206 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/9ed63e45b247c30a02e8e59b4d24fccbe8644876"/> |
|
207 |
+ <title> |
|
208 |
+ Merge pull request #397 from cantino/update_rails_and_gems |
|
209 |
+ </title> |
|
210 |
+ <updated>2014-07-05T16:34:29-07:00</updated> |
|
211 |
+ <media:thumbnail height="30" width="30" url="https://avatars3.githubusercontent.com/u/83835?s=30"/> |
|
212 |
+ <author> |
|
213 |
+ <name>cantino</name> |
|
214 |
+ <uri>https://github.com/cantino</uri> |
|
215 |
+ </author> |
|
216 |
+ <content type="html"> |
|
217 |
+ <pre style='white-space:pre-wrap;width:81ex'>Merge pull request #397 from cantino/update_rails_and_gems |
|
218 |
+ |
|
219 |
+upgrade rails and gems</pre> |
|
220 |
+ </content> |
|
221 |
+ </entry> |
|
222 |
+ <entry> |
|
223 |
+ <id>tag:github.com,2008:Grit::Commit/87a7abda23a82305d7050ac0bb400ce36c863d01</id> |
|
224 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/87a7abda23a82305d7050ac0bb400ce36c863d01"/> |
|
225 |
+ <title> |
|
226 |
+ upgrade rails and gems |
|
227 |
+ </title> |
|
228 |
+ <updated>2014-07-05T08:01:36-07:00</updated> |
|
229 |
+ <media:thumbnail height="30" width="30" url="https://avatars3.githubusercontent.com/u/83835?s=30"/> |
|
230 |
+ <author> |
|
231 |
+ <name>cantino</name> |
|
232 |
+ <uri>https://github.com/cantino</uri> |
|
233 |
+ </author> |
|
234 |
+ <content type="html"> |
|
235 |
+ <pre style='white-space:pre-wrap;width:81ex'>upgrade rails and gems</pre> |
|
236 |
+ </content> |
|
237 |
+ </entry> |
|
238 |
+ <entry> |
|
239 |
+ <id>tag:github.com,2008:Grit::Commit/ea7594fa976fe24bb7024b6e3e0d2881dd86033a</id> |
|
240 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/ea7594fa976fe24bb7024b6e3e0d2881dd86033a"/> |
|
241 |
+ <title> |
|
242 |
+ Merge pull request #396 from knu/show_propagate_immediately |
|
243 |
+ </title> |
|
244 |
+ <updated>2014-07-03T20:50:40-07:00</updated> |
|
245 |
+ <media:thumbnail height="30" width="30" url="https://avatars3.githubusercontent.com/u/83835?s=30"/> |
|
246 |
+ <author> |
|
247 |
+ <name>cantino</name> |
|
248 |
+ <uri>https://github.com/cantino</uri> |
|
249 |
+ </author> |
|
250 |
+ <content type="html"> |
|
251 |
+ <pre style='white-space:pre-wrap;width:81ex'>Merge pull request #396 from knu/show_propagate_immediately |
|
252 |
+ |
|
253 |
+Make propagate_immediately more visible in agent details and the diagram.</pre> |
|
254 |
+ </content> |
|
255 |
+ </entry> |
|
256 |
+ <entry> |
|
257 |
+ <id>tag:github.com,2008:Grit::Commit/0e80f5341587aace2c023b06eb9265b776ac4535</id> |
|
258 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/0e80f5341587aace2c023b06eb9265b776ac4535"/> |
|
259 |
+ <title> |
|
260 |
+ Dashed line in a diagram indicates propagate_immediately being false. |
|
261 |
+ </title> |
|
262 |
+ <updated>2014-07-04T03:42:52+09:00</updated> |
|
263 |
+ <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/10236?s=30"/> |
|
264 |
+ <author> |
|
265 |
+ <name>knu</name> |
|
266 |
+ <uri>https://github.com/knu</uri> |
|
267 |
+ </author> |
|
268 |
+ <content type="html"> |
|
269 |
+ <pre style='white-space:pre-wrap;width:81ex'>Dashed line in a diagram indicates propagate_immediately being false.</pre> |
|
270 |
+ </content> |
|
271 |
+ </entry> |
|
272 |
+ <entry> |
|
273 |
+ <id>tag:github.com,2008:Grit::Commit/cf9cdfb3ac9d47b7fdf5d7669577c964bee9a186</id> |
|
274 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/cf9cdfb3ac9d47b7fdf5d7669577c964bee9a186"/> |
|
275 |
+ <title> |
|
276 |
+ Show the propagate_immediately flag in agent details. |
|
277 |
+ </title> |
|
278 |
+ <updated>2014-07-04T02:53:31+09:00</updated> |
|
279 |
+ <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/10236?s=30"/> |
|
280 |
+ <author> |
|
281 |
+ <name>knu</name> |
|
282 |
+ <uri>https://github.com/knu</uri> |
|
283 |
+ </author> |
|
284 |
+ <content type="html"> |
|
285 |
+ <pre style='white-space:pre-wrap;width:81ex'>Show the propagate_immediately flag in agent details.</pre> |
|
286 |
+ </content> |
|
287 |
+ </entry> |
|
288 |
+ <entry> |
|
289 |
+ <id>tag:github.com,2008:Grit::Commit/b1128335b8de98afc5cad1b2ca5573e3bab1da1d</id> |
|
290 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/b1128335b8de98afc5cad1b2ca5573e3bab1da1d"/> |
|
291 |
+ <title> |
|
292 |
+ Merge pull request #389 from dsander/silence_worker_status |
|
293 |
+ </title> |
|
294 |
+ <updated>2014-07-01T21:47:40-07:00</updated> |
|
295 |
+ <media:thumbnail height="30" width="30" url="https://avatars3.githubusercontent.com/u/83835?s=30"/> |
|
296 |
+ <author> |
|
297 |
+ <name>cantino</name> |
|
298 |
+ <uri>https://github.com/cantino</uri> |
|
299 |
+ </author> |
|
300 |
+ <content type="html"> |
|
301 |
+ <pre style='white-space:pre-wrap;width:81ex'>Merge pull request #389 from dsander/silence_worker_status |
|
302 |
+ |
|
303 |
+Supress logging for requests to the /worker_status</pre> |
|
304 |
+ </content> |
|
305 |
+ </entry> |
|
306 |
+ <entry> |
|
307 |
+ <id>tag:github.com,2008:Grit::Commit/d25e670b1c040f78eb648120c117853421d522c3</id> |
|
308 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/d25e670b1c040f78eb648120c117853421d522c3"/> |
|
309 |
+ <title> |
|
310 |
+ Merge pull request #393 from CloCkWeRX/google_calendar |
|
311 |
+ </title> |
|
312 |
+ <updated>2014-07-01T21:47:16-07:00</updated> |
|
313 |
+ <media:thumbnail height="30" width="30" url="https://avatars3.githubusercontent.com/u/83835?s=30"/> |
|
314 |
+ <author> |
|
315 |
+ <name>cantino</name> |
|
316 |
+ <uri>https://github.com/cantino</uri> |
|
317 |
+ </author> |
|
318 |
+ <content type="html"> |
|
319 |
+ <pre style='white-space:pre-wrap;width:81ex'>Merge pull request #393 from CloCkWeRX/google_calendar |
|
320 |
+ |
|
321 |
+Add Google calendar publish agent</pre> |
|
322 |
+ </content> |
|
323 |
+ </entry> |
|
324 |
+ <entry> |
|
325 |
+ <id>tag:github.com,2008:Grit::Commit/d7b0e35aaaafec3032d3fe271b426f1e9d3727b4</id> |
|
326 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/d7b0e35aaaafec3032d3fe271b426f1e9d3727b4"/> |
|
327 |
+ <title> |
|
328 |
+ switch to cantino-twitter-stream |
|
329 |
+ </title> |
|
330 |
+ <updated>2014-07-01T21:36:38-07:00</updated> |
|
331 |
+ <media:thumbnail height="30" width="30" url="https://avatars3.githubusercontent.com/u/83835?s=30"/> |
|
332 |
+ <author> |
|
333 |
+ <name>cantino</name> |
|
334 |
+ <uri>https://github.com/cantino</uri> |
|
335 |
+ </author> |
|
336 |
+ <content type="html"> |
|
337 |
+ <pre style='white-space:pre-wrap;width:81ex'>switch to cantino-twitter-stream</pre> |
|
338 |
+ </content> |
|
339 |
+ </entry> |
|
340 |
+ <entry> |
|
341 |
+ <id>tag:github.com,2008:Grit::Commit/d465158f77dcd9078697e6167b50abbfdfa8b1af</id> |
|
342 |
+ <link type="text/html" rel="alternate" href="https://github.com/cantino/huginn/commit/d465158f77dcd9078697e6167b50abbfdfa8b1af"/> |
|
343 |
+ <title> |
|
344 |
+ Shift to dev group |
|
345 |
+ </title> |
|
346 |
+ <updated>2014-07-01T16:37:47+09:30</updated> |
|
347 |
+ <media:thumbnail height="30" width="30" url="https://avatars3.githubusercontent.com/u/365751?s=30"/> |
|
348 |
+ <author> |
|
349 |
+ <name>CloCkWeRX</name> |
|
350 |
+ <uri>https://github.com/CloCkWeRX</uri> |
|
351 |
+ </author> |
|
352 |
+ <content type="html"> |
|
353 |
+ <pre style='white-space:pre-wrap;width:81ex'>Shift to dev group</pre> |
|
354 |
+ </content> |
|
355 |
+ </entry> |
|
356 |
+</feed> |
@@ -0,0 +1,2601 @@ |
||
1 |
+{ |
|
2 |
+ "kind": "discovery#restDescription", |
|
3 |
+ "etag": "\"C11OM5Qtr9122-scy_WeqND9D3o/icy_kevyvyjgCKjN6s1gb_9TUZs\"", |
|
4 |
+ "discoveryVersion": "v1", |
|
5 |
+ "id": "calendar:v3", |
|
6 |
+ "name": "calendar", |
|
7 |
+ "version": "v3", |
|
8 |
+ "title": "Calendar API", |
|
9 |
+ "description": "Lets you manipulate events and other calendar data.", |
|
10 |
+ "ownerDomain": "google.com", |
|
11 |
+ "ownerName": "Google", |
|
12 |
+ "icons": { |
|
13 |
+ "x16": "http://www.google.com/images/icons/product/calendar-16.png", |
|
14 |
+ "x32": "http://www.google.com/images/icons/product/calendar-32.png" |
|
15 |
+ }, |
|
16 |
+ "documentationLink": "https://developers.google.com/google-apps/calendar/firstapp", |
|
17 |
+ "protocol": "rest", |
|
18 |
+ "baseUrl": "https://www.googleapis.com/calendar/v3/", |
|
19 |
+ "basePath": "/calendar/v3/", |
|
20 |
+ "rootUrl": "https://www.googleapis.com/", |
|
21 |
+ "servicePath": "calendar/v3/", |
|
22 |
+ "batchPath": "batch", |
|
23 |
+ "parameters": { |
|
24 |
+ "alt": { |
|
25 |
+ "type": "string", |
|
26 |
+ "description": "Data format for the response.", |
|
27 |
+ "default": "json", |
|
28 |
+ "enum": [ |
|
29 |
+ "json" |
|
30 |
+ ], |
|
31 |
+ "enumDescriptions": [ |
|
32 |
+ "Responses with Content-Type of application/json" |
|
33 |
+ ], |
|
34 |
+ "location": "query" |
|
35 |
+ }, |
|
36 |
+ "fields": { |
|
37 |
+ "type": "string", |
|
38 |
+ "description": "Selector specifying which fields to include in a partial response.", |
|
39 |
+ "location": "query" |
|
40 |
+ }, |
|
41 |
+ "key": { |
|
42 |
+ "type": "string", |
|
43 |
+ "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", |
|
44 |
+ "location": "query" |
|
45 |
+ }, |
|
46 |
+ "oauth_token": { |
|
47 |
+ "type": "string", |
|
48 |
+ "description": "OAuth 2.0 token for the current user.", |
|
49 |
+ "location": "query" |
|
50 |
+ }, |
|
51 |
+ "prettyPrint": { |
|
52 |
+ "type": "boolean", |
|
53 |
+ "description": "Returns response with indentations and line breaks.", |
|
54 |
+ "default": "true", |
|
55 |
+ "location": "query" |
|
56 |
+ }, |
|
57 |
+ "quotaUser": { |
|
58 |
+ "type": "string", |
|
59 |
+ "description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.", |
|
60 |
+ "location": "query" |
|
61 |
+ }, |
|
62 |
+ "userIp": { |
|
63 |
+ "type": "string", |
|
64 |
+ "description": "IP address of the site where the request originates. Use this if you want to enforce per-user limits.", |
|
65 |
+ "location": "query" |
|
66 |
+ } |
|
67 |
+ }, |
|
68 |
+ "auth": { |
|
69 |
+ "oauth2": { |
|
70 |
+ "scopes": { |
|
71 |
+ "https://www.googleapis.com/auth/calendar": { |
|
72 |
+ "description": "Manage your calendars" |
|
73 |
+ }, |
|
74 |
+ "https://www.googleapis.com/auth/calendar.readonly": { |
|
75 |
+ "description": "View your calendars" |
|
76 |
+ } |
|
77 |
+ } |
|
78 |
+ } |
|
79 |
+ }, |
|
80 |
+ "schemas": { |
|
81 |
+ "Acl": { |
|
82 |
+ "id": "Acl", |
|
83 |
+ "type": "object", |
|
84 |
+ "properties": { |
|
85 |
+ "etag": { |
|
86 |
+ "type": "string", |
|
87 |
+ "description": "ETag of the collection." |
|
88 |
+ }, |
|
89 |
+ "items": { |
|
90 |
+ "type": "array", |
|
91 |
+ "description": "List of rules on the access control list.", |
|
92 |
+ "items": { |
|
93 |
+ "$ref": "AclRule" |
|
94 |
+ } |
|
95 |
+ }, |
|
96 |
+ "kind": { |
|
97 |
+ "type": "string", |
|
98 |
+ "description": "Type of the collection (\"calendar#acl\").", |
|
99 |
+ "default": "calendar#acl" |
|
100 |
+ }, |
|
101 |
+ "nextPageToken": { |
|
102 |
+ "type": "string", |
|
103 |
+ "description": "Token used to access the next page of this result. Omitted if no further results are available, in which case nextSyncToken is provided." |
|
104 |
+ }, |
|
105 |
+ "nextSyncToken": { |
|
106 |
+ "type": "string", |
|
107 |
+ "description": "Token used at a later point in time to retrieve only the entries that have changed since this result was returned. Omitted if further results are available, in which case nextPageToken is provided." |
|
108 |
+ } |
|
109 |
+ } |
|
110 |
+ }, |
|
111 |
+ "AclRule": { |
|
112 |
+ "id": "AclRule", |
|
113 |
+ "type": "object", |
|
114 |
+ "properties": { |
|
115 |
+ "etag": { |
|
116 |
+ "type": "string", |
|
117 |
+ "description": "ETag of the resource." |
|
118 |
+ }, |
|
119 |
+ "id": { |
|
120 |
+ "type": "string", |
|
121 |
+ "description": "Identifier of the ACL rule." |
|
122 |
+ }, |
|
123 |
+ "kind": { |
|
124 |
+ "type": "string", |
|
125 |
+ "description": "Type of the resource (\"calendar#aclRule\").", |
|
126 |
+ "default": "calendar#aclRule" |
|
127 |
+ }, |
|
128 |
+ "role": { |
|
129 |
+ "type": "string", |
|
130 |
+ "description": "The role assigned to the scope. Possible values are: \n- \"none\" - Provides no access. \n- \"freeBusyReader\" - Provides read access to free/busy information. \n- \"reader\" - Provides read access to the calendar. Private events will appear to users with reader access, but event details will be hidden. \n- \"writer\" - Provides read and write access to the calendar. Private events will appear to users with writer access, and event details will be visible. \n- \"owner\" - Provides ownership of the calendar. This role has all of the permissions of the writer role with the additional ability to see and manipulate ACLs.", |
|
131 |
+ "annotations": { |
|
132 |
+ "required": [ |
|
133 |
+ "calendar.acl.insert" |
|
134 |
+ ] |
|
135 |
+ } |
|
136 |
+ }, |
|
137 |
+ "scope": { |
|
138 |
+ "type": "object", |
|
139 |
+ "description": "The scope of the rule.", |
|
140 |
+ "properties": { |
|
141 |
+ "type": { |
|
142 |
+ "type": "string", |
|
143 |
+ "description": "The type of the scope. Possible values are: \n- \"default\" - The public scope. This is the default value. \n- \"user\" - Limits the scope to a single user. \n- \"group\" - Limits the scope to a group. \n- \"domain\" - Limits the scope to a domain. Note: The permissions granted to the \"default\", or public, scope apply to any user, authenticated or not.", |
|
144 |
+ "annotations": { |
|
145 |
+ "required": [ |
|
146 |
+ "calendar.acl.insert" |
|
147 |
+ ] |
|
148 |
+ } |
|
149 |
+ }, |
|
150 |
+ "value": { |
|
151 |
+ "type": "string", |
|
152 |
+ "description": "The email address of a user or group, or the name of a domain, depending on the scope type. Omitted for type \"default\"." |
|
153 |
+ } |
|
154 |
+ }, |
|
155 |
+ "annotations": { |
|
156 |
+ "required": [ |
|
157 |
+ "calendar.acl.insert" |
|
158 |
+ ] |
|
159 |
+ } |
|
160 |
+ } |
|
161 |
+ } |
|
162 |
+ }, |
|
163 |
+ "Calendar": { |
|
164 |
+ "id": "Calendar", |
|
165 |
+ "type": "object", |
|
166 |
+ "properties": { |
|
167 |
+ "description": { |
|
168 |
+ "type": "string", |
|
169 |
+ "description": "Description of the calendar. Optional." |
|
170 |
+ }, |
|
171 |
+ "etag": { |
|
172 |
+ "type": "string", |
|
173 |
+ "description": "ETag of the resource." |
|
174 |
+ }, |
|
175 |
+ "id": { |
|
176 |
+ "type": "string", |
|
177 |
+ "description": "Identifier of the calendar." |
|
178 |
+ }, |
|
179 |
+ "kind": { |
|
180 |
+ "type": "string", |
|
181 |
+ "description": "Type of the resource (\"calendar#calendar\").", |
|
182 |
+ "default": "calendar#calendar" |
|
183 |
+ }, |
|
184 |
+ "location": { |
|
185 |
+ "type": "string", |
|
186 |
+ "description": "Geographic location of the calendar as free-form text. Optional." |
|
187 |
+ }, |
|
188 |
+ "summary": { |
|
189 |
+ "type": "string", |
|
190 |
+ "description": "Title of the calendar.", |
|
191 |
+ "annotations": { |
|
192 |
+ "required": [ |
|
193 |
+ "calendar.calendars.insert" |
|
194 |
+ ] |
|
195 |
+ } |
|
196 |
+ }, |
|
197 |
+ "timeZone": { |
|
198 |
+ "type": "string", |
|
199 |
+ "description": "The time zone of the calendar. Optional." |
|
200 |
+ } |
|
201 |
+ } |
|
202 |
+ }, |
|
203 |
+ "CalendarList": { |
|
204 |
+ "id": "CalendarList", |
|
205 |
+ "type": "object", |
|
206 |
+ "properties": { |
|
207 |
+ "etag": { |
|
208 |
+ "type": "string", |
|
209 |
+ "description": "ETag of the collection." |
|
210 |
+ }, |
|
211 |
+ "items": { |
|
212 |
+ "type": "array", |
|
213 |
+ "description": "Calendars that are present on the user's calendar list.", |
|
214 |
+ "items": { |
|
215 |
+ "$ref": "CalendarListEntry" |
|
216 |
+ } |
|
217 |
+ }, |
|
218 |
+ "kind": { |
|
219 |
+ "type": "string", |
|
220 |
+ "description": "Type of the collection (\"calendar#calendarList\").", |
|
221 |
+ "default": "calendar#calendarList" |
|
222 |
+ }, |
|
223 |
+ "nextPageToken": { |
|
224 |
+ "type": "string", |
|
225 |
+ "description": "Token used to access the next page of this result. Omitted if no further results are available, in which case nextSyncToken is provided." |
|
226 |
+ }, |
|
227 |
+ "nextSyncToken": { |
|
228 |
+ "type": "string", |
|
229 |
+ "description": "Token used at a later point in time to retrieve only the entries that have changed since this result was returned. Omitted if further results are available, in which case nextPageToken is provided." |
|
230 |
+ } |
|
231 |
+ } |
|
232 |
+ }, |
|
233 |
+ "CalendarListEntry": { |
|
234 |
+ "id": "CalendarListEntry", |
|
235 |
+ "type": "object", |
|
236 |
+ "properties": { |
|
237 |
+ "accessRole": { |
|
238 |
+ "type": "string", |
|
239 |
+ "description": "The effective access role that the authenticated user has on the calendar. Read-only. Possible values are: \n- \"freeBusyReader\" - Provides read access to free/busy information. \n- \"reader\" - Provides read access to the calendar. Private events will appear to users with reader access, but event details will be hidden. \n- \"writer\" - Provides read and write access to the calendar. Private events will appear to users with writer access, and event details will be visible. \n- \"owner\" - Provides ownership of the calendar. This role has all of the permissions of the writer role with the additional ability to see and manipulate ACLs." |
|
240 |
+ }, |
|
241 |
+ "backgroundColor": { |
|
242 |
+ "type": "string", |
|
243 |
+ "description": "The main color of the calendar in the hexadecimal format \"#0088aa\". This property supersedes the index-based colorId property. Optional." |
|
244 |
+ }, |
|
245 |
+ "colorId": { |
|
246 |
+ "type": "string", |
|
247 |
+ "description": "The color of the calendar. This is an ID referring to an entry in the calendar section of the colors definition (see the colors endpoint). Optional." |
|
248 |
+ }, |
|
249 |
+ "defaultReminders": { |
|
250 |
+ "type": "array", |
|
251 |
+ "description": "The default reminders that the authenticated user has for this calendar.", |
|
252 |
+ "items": { |
|
253 |
+ "$ref": "EventReminder" |
|
254 |
+ } |
|
255 |
+ }, |
|
256 |
+ "deleted": { |
|
257 |
+ "type": "boolean", |
|
258 |
+ "description": "Whether this calendar list entry has been deleted from the calendar list. Read-only. Optional. The default is False.", |
|
259 |
+ "default": "false" |
|
260 |
+ }, |
|
261 |
+ "description": { |
|
262 |
+ "type": "string", |
|
263 |
+ "description": "Description of the calendar. Optional. Read-only." |
|
264 |
+ }, |
|
265 |
+ "etag": { |
|
266 |
+ "type": "string", |
|
267 |
+ "description": "ETag of the resource." |
|
268 |
+ }, |
|
269 |
+ "foregroundColor": { |
|
270 |
+ "type": "string", |
|
271 |
+ "description": "The foreground color of the calendar in the hexadecimal format \"#ffffff\". This property supersedes the index-based colorId property. Optional." |
|
272 |
+ }, |
|
273 |
+ "hidden": { |
|
274 |
+ "type": "boolean", |
|
275 |
+ "description": "Whether the calendar has been hidden from the list. Optional. The default is False.", |
|
276 |
+ "default": "false" |
|
277 |
+ }, |
|
278 |
+ "id": { |
|
279 |
+ "type": "string", |
|
280 |
+ "description": "Identifier of the calendar.", |
|
281 |
+ "annotations": { |
|
282 |
+ "required": [ |
|
283 |
+ "calendar.calendarList.insert" |
|
284 |
+ ] |
|
285 |
+ } |
|
286 |
+ }, |
|
287 |
+ "kind": { |
|
288 |
+ "type": "string", |
|
289 |
+ "description": "Type of the resource (\"calendar#calendarListEntry\").", |
|
290 |
+ "default": "calendar#calendarListEntry" |
|
291 |
+ }, |
|
292 |
+ "location": { |
|
293 |
+ "type": "string", |
|
294 |
+ "description": "Geographic location of the calendar as free-form text. Optional. Read-only." |
|
295 |
+ }, |
|
296 |
+ "notificationSettings": { |
|
297 |
+ "type": "object", |
|
298 |
+ "description": "The notifications that the authenticated user is receiving for this calendar.", |
|
299 |
+ "properties": { |
|
300 |
+ "notifications": { |
|
301 |
+ "type": "array", |
|
302 |
+ "description": "The list of notifications set for this calendar.", |
|
303 |
+ "items": { |
|
304 |
+ "$ref": "CalendarNotification" |
|
305 |
+ } |
|
306 |
+ } |
|
307 |
+ } |
|
308 |
+ }, |
|
309 |
+ "primary": { |
|
310 |
+ "type": "boolean", |
|
311 |
+ "description": "Whether the calendar is the primary calendar of the authenticated user. Read-only. Optional. The default is False.", |
|
312 |
+ "default": "false" |
|
313 |
+ }, |
|
314 |
+ "selected": { |
|
315 |
+ "type": "boolean", |
|
316 |
+ "description": "Whether the calendar content shows up in the calendar UI. Optional. The default is False.", |
|
317 |
+ "default": "false" |
|
318 |
+ }, |
|
319 |
+ "summary": { |
|
320 |
+ "type": "string", |
|
321 |
+ "description": "Title of the calendar. Read-only." |
|
322 |
+ }, |
|
323 |
+ "summaryOverride": { |
|
324 |
+ "type": "string", |
|
325 |
+ "description": "The summary that the authenticated user has set for this calendar. Optional." |
|
326 |
+ }, |
|
327 |
+ "timeZone": { |
|
328 |
+ "type": "string", |
|
329 |
+ "description": "The time zone of the calendar. Optional. Read-only." |
|
330 |
+ } |
|
331 |
+ } |
|
332 |
+ }, |
|
333 |
+ "CalendarNotification": { |
|
334 |
+ "id": "CalendarNotification", |
|
335 |
+ "type": "object", |
|
336 |
+ "properties": { |
|
337 |
+ "method": { |
|
338 |
+ "type": "string", |
|
339 |
+ "description": "The method used to deliver the notification. Possible values are: \n- \"email\" - Reminders are sent via email. \n- \"sms\" - Reminders are sent via SMS. This value is read-only and is ignored on inserts and updates.", |
|
340 |
+ "annotations": { |
|
341 |
+ "required": [ |
|
342 |
+ "calendar.calendarList.insert", |
|
343 |
+ "calendar.calendarList.update" |
|
344 |
+ ] |
|
345 |
+ } |
|
346 |
+ }, |
|
347 |
+ "type": { |
|
348 |
+ "type": "string", |
|
349 |
+ "description": "The type of notification. Possible values are: \n- \"eventCreation\" - Notification sent when a new event is put on the calendar. \n- \"eventChange\" - Notification sent when an event is changed. \n- \"eventCancellation\" - Notification sent when an event is cancelled. \n- \"eventResponse\" - Notification sent when an event is changed. \n- \"agenda\" - An agenda with the events of the day (sent out in the morning).", |
|
350 |
+ "annotations": { |
|
351 |
+ "required": [ |
|
352 |
+ "calendar.calendarList.insert", |
|
353 |
+ "calendar.calendarList.update" |
|
354 |
+ ] |
|
355 |
+ } |
|
356 |
+ } |
|
357 |
+ } |
|
358 |
+ }, |
|
359 |
+ "Channel": { |
|
360 |
+ "id": "Channel", |
|
361 |
+ "type": "object", |
|
362 |
+ "properties": { |
|
363 |
+ "address": { |
|
364 |
+ "type": "string", |
|
365 |
+ "description": "The address where notifications are delivered for this channel." |
|
366 |
+ }, |
|
367 |
+ "expiration": { |
|
368 |
+ "type": "string", |
|
369 |
+ "description": "Date and time of notification channel expiration, expressed as a Unix timestamp, in milliseconds. Optional.", |
|
370 |
+ "format": "int64" |
|
371 |
+ }, |
|
372 |
+ "id": { |
|
373 |
+ "type": "string", |
|
374 |
+ "description": "A UUID or similar unique string that identifies this channel." |
|
375 |
+ }, |
|
376 |
+ "kind": { |
|
377 |
+ "type": "string", |
|
378 |
+ "description": "Identifies this as a notification channel used to watch for changes to a resource. Value: the fixed string \"api#channel\".", |
|
379 |
+ "default": "api#channel" |
|
380 |
+ }, |
|
381 |
+ "params": { |
|
382 |
+ "type": "object", |
|
383 |
+ "description": "Additional parameters controlling delivery channel behavior. Optional.", |
|
384 |
+ "additionalProperties": { |
|
385 |
+ "type": "string", |
|
386 |
+ "description": "Declares a new parameter by name." |
|
387 |
+ } |
|
388 |
+ }, |
|
389 |
+ "payload": { |
|
390 |
+ "type": "boolean", |
|
391 |
+ "description": "A Boolean value to indicate whether payload is wanted. Optional." |
|
392 |
+ }, |
|
393 |
+ "resourceId": { |
|
394 |
+ "type": "string", |
|
395 |
+ "description": "An opaque ID that identifies the resource being watched on this channel. Stable across different API versions." |
|
396 |
+ }, |
|
397 |
+ "resourceUri": { |
|
398 |
+ "type": "string", |
|
399 |
+ "description": "A version-specific identifier for the watched resource." |
|
400 |
+ }, |
|
401 |
+ "token": { |
|
402 |
+ "type": "string", |
|
403 |
+ "description": "An arbitrary string delivered to the target address with each notification delivered over this channel. Optional." |
|
404 |
+ }, |
|
405 |
+ "type": { |
|
406 |
+ "type": "string", |
|
407 |
+ "description": "The type of delivery mechanism used for this channel." |
|
408 |
+ } |
|
409 |
+ } |
|
410 |
+ }, |
|
411 |
+ "ColorDefinition": { |
|
412 |
+ "id": "ColorDefinition", |
|
413 |
+ "type": "object", |
|
414 |
+ "properties": { |
|
415 |
+ "background": { |
|
416 |
+ "type": "string", |
|
417 |
+ "description": "The background color associated with this color definition." |
|
418 |
+ }, |
|
419 |
+ "foreground": { |
|
420 |
+ "type": "string", |
|
421 |
+ "description": "The foreground color that can be used to write on top of a background with 'background' color." |
|
422 |
+ } |
|
423 |
+ } |
|
424 |
+ }, |
|
425 |
+ "Colors": { |
|
426 |
+ "id": "Colors", |
|
427 |
+ "type": "object", |
|
428 |
+ "properties": { |
|
429 |
+ "calendar": { |
|
430 |
+ "type": "object", |
|
431 |
+ "description": "Palette of calendar colors, mapping from the color ID to its definition. A calendarListEntry resource refers to one of these color IDs in its color field. Read-only.", |
|
432 |
+ "additionalProperties": { |
|
433 |
+ "$ref": "ColorDefinition", |
|
434 |
+ "description": "A calendar color defintion." |
|
435 |
+ } |
|
436 |
+ }, |
|
437 |
+ "event": { |
|
438 |
+ "type": "object", |
|
439 |
+ "description": "Palette of event colors, mapping from the color ID to its definition. An event resource may refer to one of these color IDs in its color field. Read-only.", |
|
440 |
+ "additionalProperties": { |
|
441 |
+ "$ref": "ColorDefinition", |
|
442 |
+ "description": "An event color definition." |
|
443 |
+ } |
|
444 |
+ }, |
|
445 |
+ "kind": { |
|
446 |
+ "type": "string", |
|
447 |
+ "description": "Type of the resource (\"calendar#colors\").", |
|
448 |
+ "default": "calendar#colors" |
|
449 |
+ }, |
|
450 |
+ "updated": { |
|
451 |
+ "type": "string", |
|
452 |
+ "description": "Last modification time of the color palette (as a RFC 3339 timestamp). Read-only.", |
|
453 |
+ "format": "date-time" |
|
454 |
+ } |
|
455 |
+ } |
|
456 |
+ }, |
|
457 |
+ "Error": { |
|
458 |
+ "id": "Error", |
|
459 |
+ "type": "object", |
|
460 |
+ "properties": { |
|
461 |
+ "domain": { |
|
462 |
+ "type": "string", |
|
463 |
+ "description": "Domain, or broad category, of the error." |
|
464 |
+ }, |
|
465 |
+ "reason": { |
|
466 |
+ "type": "string", |
|
467 |
+ "description": "Specific reason for the error. Some of the possible values are: \n- \"groupTooBig\" - The group of users requested is too large for a single query. \n- \"tooManyCalendarsRequested\" - The number of calendars requested is too large for a single query. \n- \"notFound\" - The requested resource was not found. \n- \"internalError\" - The API service has encountered an internal error. Additional error types may be added in the future, so clients should gracefully handle additional error statuses not included in this list." |
|
468 |
+ } |
|
469 |
+ } |
|
470 |
+ }, |
|
471 |
+ "Event": { |
|
472 |
+ "id": "Event", |
|
473 |
+ "type": "object", |
|
474 |
+ "properties": { |
|
475 |
+ "anyoneCanAddSelf": { |
|
476 |
+ "type": "boolean", |
|
477 |
+ "description": "Whether anyone can invite themselves to the event. Optional. The default is False.", |
|
478 |
+ "default": "false" |
|
479 |
+ }, |
|
480 |
+ "attendees": { |
|
481 |
+ "type": "array", |
|
482 |
+ "description": "The attendees of the event.", |
|
483 |
+ "items": { |
|
484 |
+ "$ref": "EventAttendee" |
|
485 |
+ } |
|
486 |
+ }, |
|
487 |
+ "attendeesOmitted": { |
|
488 |
+ "type": "boolean", |
|
489 |
+ "description": "Whether attendees may have been omitted from the event's representation. When retrieving an event, this may be due to a restriction specified by the maxAttendee query parameter. When updating an event, this can be used to only update the participant's response. Optional. The default is False.", |
|
490 |
+ "default": "false" |
|
491 |
+ }, |
|
492 |
+ "colorId": { |
|
493 |
+ "type": "string", |
|
494 |
+ "description": "The color of the event. This is an ID referring to an entry in the event section of the colors definition (see the colors endpoint). Optional." |
|
495 |
+ }, |
|
496 |
+ "created": { |
|
497 |
+ "type": "string", |
|
498 |
+ "description": "Creation time of the event (as a RFC 3339 timestamp). Read-only.", |
|
499 |
+ "format": "date-time" |
|
500 |
+ }, |
|
501 |
+ "creator": { |
|
502 |
+ "type": "object", |
|
503 |
+ "description": "The creator of the event. Read-only.", |
|
504 |
+ "properties": { |
|
505 |
+ "displayName": { |
|
506 |
+ "type": "string", |
|
507 |
+ "description": "The creator's name, if available." |
|
508 |
+ }, |
|
509 |
+ "email": { |
|
510 |
+ "type": "string", |
|
511 |
+ "description": "The creator's email address, if available." |
|
512 |
+ }, |
|
513 |
+ "id": { |
|
514 |
+ "type": "string", |
|
515 |
+ "description": "The creator's Profile ID, if available." |
|
516 |
+ }, |
|
517 |
+ "self": { |
|
518 |
+ "type": "boolean", |
|
519 |
+ "description": "Whether the creator corresponds to the calendar on which this copy of the event appears. Read-only. The default is False.", |
|
520 |
+ "default": "false" |
|
521 |
+ } |
|
522 |
+ } |
|
523 |
+ }, |
|
524 |
+ "description": { |
|
525 |
+ "type": "string", |
|
526 |
+ "description": "Description of the event. Optional." |
|
527 |
+ }, |
|
528 |
+ "end": { |
|
529 |
+ "$ref": "EventDateTime", |
|
530 |
+ "description": "The (exclusive) end time of the event. For a recurring event, this is the end time of the first instance.", |
|
531 |
+ "annotations": { |
|
532 |
+ "required": [ |
|
533 |
+ "calendar.events.import", |
|
534 |
+ "calendar.events.insert", |
|
535 |
+ "calendar.events.update" |
|
536 |
+ ] |
|
537 |
+ } |
|
538 |
+ }, |
|
539 |
+ "endTimeUnspecified": { |
|
540 |
+ "type": "boolean", |
|
541 |
+ "description": "Whether the end time is actually unspecified. An end time is still provided for compatibility reasons, even if this attribute is set to True. The default is False.", |
|
542 |
+ "default": "false" |
|
543 |
+ }, |
|
544 |
+ "etag": { |
|
545 |
+ "type": "string", |
|
546 |
+ "description": "ETag of the resource." |
|
547 |
+ }, |
|
548 |
+ "extendedProperties": { |
|
549 |
+ "type": "object", |
|
550 |
+ "description": "Extended properties of the event.", |
|
551 |
+ "properties": { |
|
552 |
+ "private": { |
|
553 |
+ "type": "object", |
|
554 |
+ "description": "Properties that are private to the copy of the event that appears on this calendar.", |
|
555 |
+ "additionalProperties": { |
|
556 |
+ "type": "string", |
|
557 |
+ "description": "The name of the private property and the corresponding value." |
|
558 |
+ } |
|
559 |
+ }, |
|
560 |
+ "shared": { |
|
561 |
+ "type": "object", |
|
562 |
+ "description": "Properties that are shared between copies of the event on other attendees' calendars.", |
|
563 |
+ "additionalProperties": { |
|
564 |
+ "type": "string", |
|
565 |
+ "description": "The name of the shared property and the corresponding value." |
|
566 |
+ } |
|
567 |
+ } |
|
568 |
+ } |
|
569 |
+ }, |
|
570 |
+ "gadget": { |
|
571 |
+ "type": "object", |
|
572 |
+ "description": "A gadget that extends this event.", |
|
573 |
+ "properties": { |
|
574 |
+ "display": { |
|
575 |
+ "type": "string", |
|
576 |
+ "description": "The gadget's display mode. Optional. Possible values are: \n- \"icon\" - The gadget displays next to the event's title in the calendar view. \n- \"chip\" - The gadget displays when the event is clicked." |
|
577 |
+ }, |
|
578 |
+ "height": { |
|
579 |
+ "type": "integer", |
|
580 |
+ "description": "The gadget's height in pixels. Optional.", |
|
581 |
+ "format": "int32" |
|
582 |
+ }, |
|
583 |
+ "iconLink": { |
|
584 |
+ "type": "string", |
|
585 |
+ "description": "The gadget's icon URL." |
|
586 |
+ }, |
|
587 |
+ "link": { |
|
588 |
+ "type": "string", |
|
589 |
+ "description": "The gadget's URL." |
|
590 |
+ }, |
|
591 |
+ "preferences": { |
|
592 |
+ "type": "object", |
|
593 |
+ "description": "Preferences.", |
|
594 |
+ "additionalProperties": { |
|
595 |
+ "type": "string", |
|
596 |
+ "description": "The preference name and corresponding value." |
|
597 |
+ } |
|
598 |
+ }, |
|
599 |
+ "title": { |
|
600 |
+ "type": "string", |
|
601 |
+ "description": "The gadget's title." |
|
602 |
+ }, |
|
603 |
+ "type": { |
|
604 |
+ "type": "string", |
|
605 |
+ "description": "The gadget's type." |
|
606 |
+ }, |
|
607 |
+ "width": { |
|
608 |
+ "type": "integer", |
|
609 |
+ "description": "The gadget's width in pixels. Optional.", |
|
610 |
+ "format": "int32" |
|
611 |
+ } |
|
612 |
+ } |
|
613 |
+ }, |
|
614 |
+ "guestsCanInviteOthers": { |
|
615 |
+ "type": "boolean", |
|
616 |
+ "description": "Whether attendees other than the organizer can invite others to the event. Optional. The default is True.", |
|
617 |
+ "default": "true" |
|
618 |
+ }, |
|
619 |
+ "guestsCanModify": { |
|
620 |
+ "type": "boolean", |
|
621 |
+ "description": "Whether attendees other than the organizer can modify the event. Optional. The default is False.", |
|
622 |
+ "default": "false" |
|
623 |
+ }, |
|
624 |
+ "guestsCanSeeOtherGuests": { |
|
625 |
+ "type": "boolean", |
|
626 |
+ "description": "Whether attendees other than the organizer can see who the event's attendees are. Optional. The default is True.", |
|
627 |
+ "default": "true" |
|
628 |
+ }, |
|
629 |
+ "hangoutLink": { |
|
630 |
+ "type": "string", |
|
631 |
+ "description": "An absolute link to the Google+ hangout associated with this event. Read-only." |
|
632 |
+ }, |
|
633 |
+ "htmlLink": { |
|
634 |
+ "type": "string", |
|
635 |
+ "description": "An absolute link to this event in the Google Calendar Web UI. Read-only." |
|
636 |
+ }, |
|
637 |
+ "iCalUID": { |
|
638 |
+ "type": "string", |
|
639 |
+ "description": "Event ID in the iCalendar format.", |
|
640 |
+ "annotations": { |
|
641 |
+ "required": [ |
|
642 |
+ "calendar.events.import" |
|
643 |
+ ] |
|
644 |
+ } |
|
645 |
+ }, |
|
646 |
+ "id": { |
|
647 |
+ "type": "string", |
|
648 |
+ "description": "Identifier of the event. When creating new single or recurring events, you can specify their IDs. Provided IDs must follow these rules: \n- characters allowed in the ID are those used in base32hex encoding, i.e. lowercase letters a-v and digits 0-9, see section 3.1.2 in RFC2938 \n- the length of the ID must be between 5 and 1024 characters \n- the ID must be unique per calendar Due to the globally distributed nature of the system, we cannot guarantee that ID collisions will be detected at event creation time. To minimize the risk of collisions we recommend using an established UUID algorithm such as one described in RFC4122." |
|
649 |
+ }, |
|
650 |
+ "kind": { |
|
651 |
+ "type": "string", |
|
652 |
+ "description": "Type of the resource (\"calendar#event\").", |
|
653 |
+ "default": "calendar#event" |
|
654 |
+ }, |
|
655 |
+ "location": { |
|
656 |
+ "type": "string", |
|
657 |
+ "description": "Geographic location of the event as free-form text. Optional." |
|
658 |
+ }, |
|
659 |
+ "locked": { |
|
660 |
+ "type": "boolean", |
|
661 |
+ "description": "Whether this is a locked event copy where no changes can be made to the main event fields \"summary\", \"description\", \"location\", \"start\", \"end\" or \"recurrence\". The default is False. Read-Only.", |
|
662 |
+ "default": "false" |
|
663 |
+ }, |
|
664 |
+ "organizer": { |
|
665 |
+ "type": "object", |
|
666 |
+ "description": "The organizer of the event. If the organizer is also an attendee, this is indicated with a separate entry in attendees with the organizer field set to True. To change the organizer, use the move operation. Read-only, except when importing an event.", |
|
667 |
+ "properties": { |
|
668 |
+ "displayName": { |
|
669 |
+ "type": "string", |
|
670 |
+ "description": "The organizer's name, if available." |
|
671 |
+ }, |
|
672 |
+ "email": { |
|
673 |
+ "type": "string", |
|
674 |
+ "description": "The organizer's email address, if available." |
|
675 |
+ }, |
|
676 |
+ "id": { |
|
677 |
+ "type": "string", |
|
678 |
+ "description": "The organizer's Profile ID, if available." |
|
679 |
+ }, |
|
680 |
+ "self": { |
|
681 |
+ "type": "boolean", |
|
682 |
+ "description": "Whether the organizer corresponds to the calendar on which this copy of the event appears. Read-only. The default is False.", |
|
683 |
+ "default": "false" |
|
684 |
+ } |
|
685 |
+ } |
|
686 |
+ }, |
|
687 |
+ "originalStartTime": { |
|
688 |
+ "$ref": "EventDateTime", |
|
689 |
+ "description": "For an instance of a recurring event, this is the time at which this event would start according to the recurrence data in the recurring event identified by recurringEventId. Immutable." |
|
690 |
+ }, |
|
691 |
+ "privateCopy": { |
|
692 |
+ "type": "boolean", |
|
693 |
+ "description": "Whether this is a private event copy where changes are not shared with other copies on other calendars. Optional. Immutable. The default is False.", |
|
694 |
+ "default": "false" |
|
695 |
+ }, |
|
696 |
+ "recurrence": { |
|
697 |
+ "type": "array", |
|
698 |
+ "description": "List of RRULE, EXRULE, RDATE and EXDATE lines for a recurring event. This field is omitted for single events or instances of recurring events.", |
|
699 |
+ "items": { |
|
700 |
+ "type": "string" |
|
701 |
+ } |
|
702 |
+ }, |
|
703 |
+ "recurringEventId": { |
|
704 |
+ "type": "string", |
|
705 |
+ "description": "For an instance of a recurring event, this is the event ID of the recurring event itself. Immutable." |
|
706 |
+ }, |
|
707 |
+ "reminders": { |
|
708 |
+ "type": "object", |
|
709 |
+ "description": "Information about the event's reminders for the authenticated user.", |
|
710 |
+ "properties": { |
|
711 |
+ "overrides": { |
|
712 |
+ "type": "array", |
|
713 |
+ "description": "If the event doesn't use the default reminders, this lists the reminders specific to the event, or, if not set, indicates that no reminders are set for this event.", |
|
714 |
+ "items": { |
|
715 |
+ "$ref": "EventReminder" |
|
716 |
+ } |
|
717 |
+ }, |
|
718 |
+ "useDefault": { |
|
719 |
+ "type": "boolean", |
|
720 |
+ "description": "Whether the default reminders of the calendar apply to the event." |
|
721 |
+ } |
|
722 |
+ } |
|
723 |
+ }, |
|
724 |
+ "sequence": { |
|
725 |
+ "type": "integer", |
|
726 |
+ "description": "Sequence number as per iCalendar.", |
|
727 |
+ "format": "int32" |
|
728 |
+ }, |
|
729 |
+ "source": { |
|
730 |
+ "type": "object", |
|
731 |
+ "description": "Source of an event from which it was created; for example a web page, an email message or any document identifiable by an URL using HTTP/HTTPS protocol. Accessible only by the creator of the event.", |
|
732 |
+ "properties": { |
|
733 |
+ "title": { |
|
734 |
+ "type": "string", |
|
735 |
+ "description": "Title of the source; for example a title of a web page or an email subject." |
|
736 |
+ }, |
|
737 |
+ "url": { |
|
738 |
+ "type": "string", |
|
739 |
+ "description": "URL of the source pointing to a resource. URL's protocol must be HTTP or HTTPS." |
|
740 |
+ } |
|
741 |
+ } |
|
742 |
+ }, |
|
743 |
+ "start": { |
|
744 |
+ "$ref": "EventDateTime", |
|
745 |
+ "description": "The (inclusive) start time of the event. For a recurring event, this is the start time of the first instance.", |
|
746 |
+ "annotations": { |
|
747 |
+ "required": [ |
|
748 |
+ "calendar.events.import", |
|
749 |
+ "calendar.events.insert", |
|
750 |
+ "calendar.events.update" |
|
751 |
+ ] |
|
752 |
+ } |
|
753 |
+ }, |
|
754 |
+ "status": { |
|
755 |
+ "type": "string", |
|
756 |
+ "description": "Status of the event. Optional. Possible values are: \n- \"confirmed\" - The event is confirmed. This is the default status. \n- \"tentative\" - The event is tentatively confirmed. \n- \"cancelled\" - The event is cancelled." |
|
757 |
+ }, |
|
758 |
+ "summary": { |
|
759 |
+ "type": "string", |
|
760 |
+ "description": "Title of the event." |
|
761 |
+ }, |
|
762 |
+ "transparency": { |
|
763 |
+ "type": "string", |
|
764 |
+ "description": "Whether the event blocks time on the calendar. Optional. Possible values are: \n- \"opaque\" - The event blocks time on the calendar. This is the default value. \n- \"transparent\" - The event does not block time on the calendar.", |
|
765 |
+ "default": "opaque" |
|
766 |
+ }, |
|
767 |
+ "updated": { |
|
768 |
+ "type": "string", |
|
769 |
+ "description": "Last modification time of the event (as a RFC 3339 timestamp). Read-only.", |
|
770 |
+ "format": "date-time" |
|
771 |
+ }, |
|
772 |
+ "visibility": { |
|
773 |
+ "type": "string", |
|
774 |
+ "description": "Visibility of the event. Optional. Possible values are: \n- \"default\" - Uses the default visibility for events on the calendar. This is the default value. \n- \"public\" - The event is public and event details are visible to all readers of the calendar. \n- \"private\" - The event is private and only event attendees may view event details. \n- \"confidential\" - The event is private. This value is provided for compatibility reasons.", |
|
775 |
+ "default": "default" |
|
776 |
+ } |
|
777 |
+ } |
|
778 |
+ }, |
|
779 |
+ "EventAttendee": { |
|
780 |
+ "id": "EventAttendee", |
|
781 |
+ "type": "object", |
|
782 |
+ "properties": { |
|
783 |
+ "additionalGuests": { |
|
784 |
+ "type": "integer", |
|
785 |
+ "description": "Number of additional guests. Optional. The default is 0.", |
|
786 |
+ "format": "int32" |
|
787 |
+ }, |
|
788 |
+ "comment": { |
|
789 |
+ "type": "string", |
|
790 |
+ "description": "The attendee's response comment. Optional." |
|
791 |
+ }, |
|
792 |
+ "displayName": { |
|
793 |
+ "type": "string", |
|
794 |
+ "description": "The attendee's name, if available. Optional." |
|
795 |
+ }, |
|
796 |
+ "email": { |
|
797 |
+ "type": "string", |
|
798 |
+ "description": "The attendee's email address, if available. This field must be present when adding an attendee.", |
|
799 |
+ "annotations": { |
|
800 |
+ "required": [ |
|
801 |
+ "calendar.events.import", |
|
802 |
+ "calendar.events.insert", |
|
803 |
+ "calendar.events.update" |
|
804 |
+ ] |
|
805 |
+ } |
|
806 |
+ }, |
|
807 |
+ "id": { |
|
808 |
+ "type": "string", |
|
809 |
+ "description": "The attendee's Profile ID, if available." |
|
810 |
+ }, |
|
811 |
+ "optional": { |
|
812 |
+ "type": "boolean", |
|
813 |
+ "description": "Whether this is an optional attendee. Optional. The default is False." |
|
814 |
+ }, |
|
815 |
+ "organizer": { |
|
816 |
+ "type": "boolean", |
|
817 |
+ "description": "Whether the attendee is the organizer of the event. Read-only. The default is False." |
|
818 |
+ }, |
|
819 |
+ "resource": { |
|
820 |
+ "type": "boolean", |
|
821 |
+ "description": "Whether the attendee is a resource. Read-only. The default is False." |
|
822 |
+ }, |
|
823 |
+ "responseStatus": { |
|
824 |
+ "type": "string", |
|
825 |
+ "description": "The attendee's response status. Possible values are: \n- \"needsAction\" - The attendee has not responded to the invitation. \n- \"declined\" - The attendee has declined the invitation. \n- \"tentative\" - The attendee has tentatively accepted the invitation. \n- \"accepted\" - The attendee has accepted the invitation." |
|
826 |
+ }, |
|
827 |
+ "self": { |
|
828 |
+ "type": "boolean", |
|
829 |
+ "description": "Whether this entry represents the calendar on which this copy of the event appears. Read-only. The default is False." |
|
830 |
+ } |
|
831 |
+ } |
|
832 |
+ }, |
|
833 |
+ "EventDateTime": { |
|
834 |
+ "id": "EventDateTime", |
|
835 |
+ "type": "object", |
|
836 |
+ "properties": { |
|
837 |
+ "date": { |
|
838 |
+ "type": "string", |
|
839 |
+ "description": "The date, in the format \"yyyy-mm-dd\", if this is an all-day event.", |
|
840 |
+ "format": "date" |
|
841 |
+ }, |
|
842 |
+ "dateTime": { |
|
843 |
+ "type": "string", |
|
844 |
+ "description": "The time, as a combined date-time value (formatted according to RFC 3339). A time zone offset is required unless a time zone is explicitly specified in timeZone.", |
|
845 |
+ "format": "date-time" |
|
846 |
+ }, |
|
847 |
+ "timeZone": { |
|
848 |
+ "type": "string", |
|
849 |
+ "description": "The name of the time zone in which the time is specified (e.g. \"Europe/Zurich\"). Optional. The default is the time zone of the calendar." |
|
850 |
+ } |
|
851 |
+ } |
|
852 |
+ }, |
|
853 |
+ "EventReminder": { |
|
854 |
+ "id": "EventReminder", |
|
855 |
+ "type": "object", |
|
856 |
+ "properties": { |
|
857 |
+ "method": { |
|
858 |
+ "type": "string", |
|
859 |
+ "description": "The method used by this reminder. Possible values are: \n- \"email\" - Reminders are sent via email. \n- \"sms\" - Reminders are sent via SMS. \n- \"popup\" - Reminders are sent via a UI popup.", |
|
860 |
+ "annotations": { |
|
861 |
+ "required": [ |
|
862 |
+ "calendar.calendarList.insert", |
|
863 |
+ "calendar.calendarList.update", |
|
864 |
+ "calendar.events.import", |
|
865 |
+ "calendar.events.insert", |
|
866 |
+ "calendar.events.update" |
|
867 |
+ ] |
|
868 |
+ } |
|
869 |
+ }, |
|
870 |
+ "minutes": { |
|
871 |
+ "type": "integer", |
|
872 |
+ "description": "Number of minutes before the start of the event when the reminder should trigger.", |
|
873 |
+ "format": "int32", |
|
874 |
+ "annotations": { |
|
875 |
+ "required": [ |
|
876 |
+ "calendar.calendarList.insert", |
|
877 |
+ "calendar.calendarList.update", |
|
878 |
+ "calendar.events.import", |
|
879 |
+ "calendar.events.insert", |
|
880 |
+ "calendar.events.update" |
|
881 |
+ ] |
|
882 |
+ } |
|
883 |
+ } |
|
884 |
+ } |
|
885 |
+ }, |
|
886 |
+ "Events": { |
|
887 |
+ "id": "Events", |
|
888 |
+ "type": "object", |
|
889 |
+ "properties": { |
|
890 |
+ "accessRole": { |
|
891 |
+ "type": "string", |
|
892 |
+ "description": "The user's access role for this calendar. Read-only. Possible values are: \n- \"none\" - The user has no access. \n- \"freeBusyReader\" - The user has read access to free/busy information. \n- \"reader\" - The user has read access to the calendar. Private events will appear to users with reader access, but event details will be hidden. \n- \"writer\" - The user has read and write access to the calendar. Private events will appear to users with writer access, and event details will be visible. \n- \"owner\" - The user has ownership of the calendar. This role has all of the permissions of the writer role with the additional ability to see and manipulate ACLs." |
|
893 |
+ }, |
|
894 |
+ "defaultReminders": { |
|
895 |
+ "type": "array", |
|
896 |
+ "description": "The default reminders on the calendar for the authenticated user. These reminders apply to all events on this calendar that do not explicitly override them (i.e. do not have reminders.useDefault set to True).", |
|
897 |
+ "items": { |
|
898 |
+ "$ref": "EventReminder" |
|
899 |
+ } |
|
900 |
+ }, |
|
901 |
+ "description": { |
|
902 |
+ "type": "string", |
|
903 |
+ "description": "Description of the calendar. Read-only." |
|
904 |
+ }, |
|
905 |
+ "etag": { |
|
906 |
+ "type": "string", |
|
907 |
+ "description": "ETag of the collection." |
|
908 |
+ }, |
|
909 |
+ "items": { |
|
910 |
+ "type": "array", |
|
911 |
+ "description": "List of events on the calendar.", |
|
912 |
+ "items": { |
|
913 |
+ "$ref": "Event" |
|
914 |
+ } |
|
915 |
+ }, |
|
916 |
+ "kind": { |
|
917 |
+ "type": "string", |
|
918 |
+ "description": "Type of the collection (\"calendar#events\").", |
|
919 |
+ "default": "calendar#events" |
|
920 |
+ }, |
|
921 |
+ "nextPageToken": { |
|
922 |
+ "type": "string", |
|
923 |
+ "description": "Token used to access the next page of this result. Omitted if no further results are available, in which case nextSyncToken is provided." |
|
924 |
+ }, |
|
925 |
+ "nextSyncToken": { |
|
926 |
+ "type": "string", |
|
927 |
+ "description": "Token used at a later point in time to retrieve only the entries that have changed since this result was returned. Omitted if further results are available, in which case nextPageToken is provided." |
|
928 |
+ }, |
|
929 |
+ "summary": { |
|
930 |
+ "type": "string", |
|
931 |
+ "description": "Title of the calendar. Read-only." |
|
932 |
+ }, |
|
933 |
+ "timeZone": { |
|
934 |
+ "type": "string", |
|
935 |
+ "description": "The time zone of the calendar. Read-only." |
|
936 |
+ }, |
|
937 |
+ "updated": { |
|
938 |
+ "type": "string", |
|
939 |
+ "description": "Last modification time of the calendar (as a RFC 3339 timestamp). Read-only.", |
|
940 |
+ "format": "date-time" |
|
941 |
+ } |
|
942 |
+ } |
|
943 |
+ }, |
|
944 |
+ "FreeBusyCalendar": { |
|
945 |
+ "id": "FreeBusyCalendar", |
|
946 |
+ "type": "object", |
|
947 |
+ "properties": { |
|
948 |
+ "busy": { |
|
949 |
+ "type": "array", |
|
950 |
+ "description": "List of time ranges during which this calendar should be regarded as busy.", |
|
951 |
+ "items": { |
|
952 |
+ "$ref": "TimePeriod" |
|
953 |
+ } |
|
954 |
+ }, |
|
955 |
+ "errors": { |
|
956 |
+ "type": "array", |
|
957 |
+ "description": "Optional error(s) (if computation for the calendar failed).", |
|
958 |
+ "items": { |
|
959 |
+ "$ref": "Error" |
|
960 |
+ } |
|
961 |
+ } |
|
962 |
+ } |
|
963 |
+ }, |
|
964 |
+ "FreeBusyGroup": { |
|
965 |
+ "id": "FreeBusyGroup", |
|
966 |
+ "type": "object", |
|
967 |
+ "properties": { |
|
968 |
+ "calendars": { |
|
969 |
+ "type": "array", |
|
970 |
+ "description": "List of calendars' identifiers within a group.", |
|
971 |
+ "items": { |
|
972 |
+ "type": "string" |
|
973 |
+ } |
|
974 |
+ }, |
|
975 |
+ "errors": { |
|
976 |
+ "type": "array", |
|
977 |
+ "description": "Optional error(s) (if computation for the group failed).", |
|
978 |
+ "items": { |
|
979 |
+ "$ref": "Error" |
|
980 |
+ } |
|
981 |
+ } |
|
982 |
+ } |
|
983 |
+ }, |
|
984 |
+ "FreeBusyRequest": { |
|
985 |
+ "id": "FreeBusyRequest", |
|
986 |
+ "type": "object", |
|
987 |
+ "properties": { |
|
988 |
+ "calendarExpansionMax": { |
|
989 |
+ "type": "integer", |
|
990 |
+ "description": "Maximal number of calendars for which FreeBusy information is to be provided. Optional.", |
|
991 |
+ "format": "int32" |
|
992 |
+ }, |
|
993 |
+ "groupExpansionMax": { |
|
994 |
+ "type": "integer", |
|
995 |
+ "description": "Maximal number of calendar identifiers to be provided for a single group. Optional. An error will be returned for a group with more members than this value.", |
|
996 |
+ "format": "int32" |
|
997 |
+ }, |
|
998 |
+ "items": { |
|
999 |
+ "type": "array", |
|
1000 |
+ "description": "List of calendars and/or groups to query.", |
|
1001 |
+ "items": { |
|
1002 |
+ "$ref": "FreeBusyRequestItem" |
|
1003 |
+ } |
|
1004 |
+ }, |
|
1005 |
+ "timeMax": { |
|
1006 |
+ "type": "string", |
|
1007 |
+ "description": "The end of the interval for the query.", |
|
1008 |
+ "format": "date-time" |
|
1009 |
+ }, |
|
1010 |
+ "timeMin": { |
|
1011 |
+ "type": "string", |
|
1012 |
+ "description": "The start of the interval for the query.", |
|
1013 |
+ "format": "date-time" |
|
1014 |
+ }, |
|
1015 |
+ "timeZone": { |
|
1016 |
+ "type": "string", |
|
1017 |
+ "description": "Time zone used in the response. Optional. The default is UTC.", |
|
1018 |
+ "default": "UTC" |
|
1019 |
+ } |
|
1020 |
+ } |
|
1021 |
+ }, |
|
1022 |
+ "FreeBusyRequestItem": { |
|
1023 |
+ "id": "FreeBusyRequestItem", |
|
1024 |
+ "type": "object", |
|
1025 |
+ "properties": { |
|
1026 |
+ "id": { |
|
1027 |
+ "type": "string", |
|
1028 |
+ "description": "The identifier of a calendar or a group." |
|
1029 |
+ } |
|
1030 |
+ } |
|
1031 |
+ }, |
|
1032 |
+ "FreeBusyResponse": { |
|
1033 |
+ "id": "FreeBusyResponse", |
|
1034 |
+ "type": "object", |
|
1035 |
+ "properties": { |
|
1036 |
+ "calendars": { |
|
1037 |
+ "type": "object", |
|
1038 |
+ "description": "List of free/busy information for calendars.", |
|
1039 |
+ "additionalProperties": { |
|
1040 |
+ "$ref": "FreeBusyCalendar", |
|
1041 |
+ "description": "Free/busy expansions for a single calendar." |
|
1042 |
+ } |
|
1043 |
+ }, |
|
1044 |
+ "groups": { |
|
1045 |
+ "type": "object", |
|
1046 |
+ "description": "Expansion of groups.", |
|
1047 |
+ "additionalProperties": { |
|
1048 |
+ "$ref": "FreeBusyGroup", |
|
1049 |
+ "description": "List of calendars that are members of this group." |
|
1050 |
+ } |
|
1051 |
+ }, |
|
1052 |
+ "kind": { |
|
1053 |
+ "type": "string", |
|
1054 |
+ "description": "Type of the resource (\"calendar#freeBusy\").", |
|
1055 |
+ "default": "calendar#freeBusy" |
|
1056 |
+ }, |
|
1057 |
+ "timeMax": { |
|
1058 |
+ "type": "string", |
|
1059 |
+ "description": "The end of the interval.", |
|
1060 |
+ "format": "date-time" |
|
1061 |
+ }, |
|
1062 |
+ "timeMin": { |
|
1063 |
+ "type": "string", |
|
1064 |
+ "description": "The start of the interval.", |
|
1065 |
+ "format": "date-time" |
|
1066 |
+ } |
|
1067 |
+ } |
|
1068 |
+ }, |
|
1069 |
+ "Setting": { |
|
1070 |
+ "id": "Setting", |
|
1071 |
+ "type": "object", |
|
1072 |
+ "properties": { |
|
1073 |
+ "etag": { |
|
1074 |
+ "type": "string", |
|
1075 |
+ "description": "ETag of the resource." |
|
1076 |
+ }, |
|
1077 |
+ "id": { |
|
1078 |
+ "type": "string", |
|
1079 |
+ "description": "The id of the user setting." |
|
1080 |
+ }, |
|
1081 |
+ "kind": { |
|
1082 |
+ "type": "string", |
|
1083 |
+ "description": "Type of the resource (\"calendar#setting\").", |
|
1084 |
+ "default": "calendar#setting" |
|
1085 |
+ }, |
|
1086 |
+ "value": { |
|
1087 |
+ "type": "string", |
|
1088 |
+ "description": "Value of the user setting. The format of the value depends on the ID of the setting. It must always be a UTF-8 string of length up to 1024 characters." |
|
1089 |
+ } |
|
1090 |
+ } |
|
1091 |
+ }, |
|
1092 |
+ "Settings": { |
|
1093 |
+ "id": "Settings", |
|
1094 |
+ "type": "object", |
|
1095 |
+ "properties": { |
|
1096 |
+ "etag": { |
|
1097 |
+ "type": "string", |
|
1098 |
+ "description": "Etag of the collection." |
|
1099 |
+ }, |
|
1100 |
+ "items": { |
|
1101 |
+ "type": "array", |
|
1102 |
+ "description": "List of user settings.", |
|
1103 |
+ "items": { |
|
1104 |
+ "$ref": "Setting" |
|
1105 |
+ } |
|
1106 |
+ }, |
|
1107 |
+ "kind": { |
|
1108 |
+ "type": "string", |
|
1109 |
+ "description": "Type of the collection (\"calendar#settings\").", |
|
1110 |
+ "default": "calendar#settings" |
|
1111 |
+ }, |
|
1112 |
+ "nextPageToken": { |
|
1113 |
+ "type": "string", |
|
1114 |
+ "description": "Token used to access the next page of this result. Omitted if no further results are available, in which case nextSyncToken is provided." |
|
1115 |
+ }, |
|
1116 |
+ "nextSyncToken": { |
|
1117 |
+ "type": "string", |
|
1118 |
+ "description": "Token used at a later point in time to retrieve only the entries that have changed since this result was returned. Omitted if further results are available, in which case nextPageToken is provided." |
|
1119 |
+ } |
|
1120 |
+ } |
|
1121 |
+ }, |
|
1122 |
+ "TimePeriod": { |
|
1123 |
+ "id": "TimePeriod", |
|
1124 |
+ "type": "object", |
|
1125 |
+ "properties": { |
|
1126 |
+ "end": { |
|
1127 |
+ "type": "string", |
|
1128 |
+ "description": "The (exclusive) end of the time period.", |
|
1129 |
+ "format": "date-time" |
|
1130 |
+ }, |
|
1131 |
+ "start": { |
|
1132 |
+ "type": "string", |
|
1133 |
+ "description": "The (inclusive) start of the time period.", |
|
1134 |
+ "format": "date-time" |
|
1135 |
+ } |
|
1136 |
+ } |
|
1137 |
+ } |
|
1138 |
+ }, |
|
1139 |
+ "resources": { |
|
1140 |
+ "acl": { |
|
1141 |
+ "methods": { |
|
1142 |
+ "delete": { |
|
1143 |
+ "id": "calendar.acl.delete", |
|
1144 |
+ "path": "calendars/{calendarId}/acl/{ruleId}", |
|
1145 |
+ "httpMethod": "DELETE", |
|
1146 |
+ "description": "Deletes an access control rule.", |
|
1147 |
+ "parameters": { |
|
1148 |
+ "calendarId": { |
|
1149 |
+ "type": "string", |
|
1150 |
+ "description": "Calendar identifier.", |
|
1151 |
+ "required": true, |
|
1152 |
+ "location": "path" |
|
1153 |
+ }, |
|
1154 |
+ "ruleId": { |
|
1155 |
+ "type": "string", |
|
1156 |
+ "description": "ACL rule identifier.", |
|
1157 |
+ "required": true, |
|
1158 |
+ "location": "path" |
|
1159 |
+ } |
|
1160 |
+ }, |
|
1161 |
+ "parameterOrder": [ |
|
1162 |
+ "calendarId", |
|
1163 |
+ "ruleId" |
|
1164 |
+ ], |
|
1165 |
+ "scopes": [ |
|
1166 |
+ "https://www.googleapis.com/auth/calendar" |
|
1167 |
+ ] |
|
1168 |
+ }, |
|
1169 |
+ "get": { |
|
1170 |
+ "id": "calendar.acl.get", |
|
1171 |
+ "path": "calendars/{calendarId}/acl/{ruleId}", |
|
1172 |
+ "httpMethod": "GET", |
|
1173 |
+ "description": "Returns an access control rule.", |
|
1174 |
+ "parameters": { |
|
1175 |
+ "calendarId": { |
|
1176 |
+ "type": "string", |
|
1177 |
+ "description": "Calendar identifier.", |
|
1178 |
+ "required": true, |
|
1179 |
+ "location": "path" |
|
1180 |
+ }, |
|
1181 |
+ "ruleId": { |
|
1182 |
+ "type": "string", |
|
1183 |
+ "description": "ACL rule identifier.", |
|
1184 |
+ "required": true, |
|
1185 |
+ "location": "path" |
|
1186 |
+ } |
|
1187 |
+ }, |
|
1188 |
+ "parameterOrder": [ |
|
1189 |
+ "calendarId", |
|
1190 |
+ "ruleId" |
|
1191 |
+ ], |
|
1192 |
+ "response": { |
|
1193 |
+ "$ref": "AclRule" |
|
1194 |
+ }, |
|
1195 |
+ "scopes": [ |
|
1196 |
+ "https://www.googleapis.com/auth/calendar", |
|
1197 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
1198 |
+ ] |
|
1199 |
+ }, |
|
1200 |
+ "insert": { |
|
1201 |
+ "id": "calendar.acl.insert", |
|
1202 |
+ "path": "calendars/{calendarId}/acl", |
|
1203 |
+ "httpMethod": "POST", |
|
1204 |
+ "description": "Creates an access control rule.", |
|
1205 |
+ "parameters": { |
|
1206 |
+ "calendarId": { |
|
1207 |
+ "type": "string", |
|
1208 |
+ "description": "Calendar identifier.", |
|
1209 |
+ "required": true, |
|
1210 |
+ "location": "path" |
|
1211 |
+ } |
|
1212 |
+ }, |
|
1213 |
+ "parameterOrder": [ |
|
1214 |
+ "calendarId" |
|
1215 |
+ ], |
|
1216 |
+ "request": { |
|
1217 |
+ "$ref": "AclRule" |
|
1218 |
+ }, |
|
1219 |
+ "response": { |
|
1220 |
+ "$ref": "AclRule" |
|
1221 |
+ }, |
|
1222 |
+ "scopes": [ |
|
1223 |
+ "https://www.googleapis.com/auth/calendar" |
|
1224 |
+ ] |
|
1225 |
+ }, |
|
1226 |
+ "list": { |
|
1227 |
+ "id": "calendar.acl.list", |
|
1228 |
+ "path": "calendars/{calendarId}/acl", |
|
1229 |
+ "httpMethod": "GET", |
|
1230 |
+ "description": "Returns the rules in the access control list for the calendar.", |
|
1231 |
+ "parameters": { |
|
1232 |
+ "calendarId": { |
|
1233 |
+ "type": "string", |
|
1234 |
+ "description": "Calendar identifier.", |
|
1235 |
+ "required": true, |
|
1236 |
+ "location": "path" |
|
1237 |
+ }, |
|
1238 |
+ "maxResults": { |
|
1239 |
+ "type": "integer", |
|
1240 |
+ "description": "Maximum number of entries returned on one result page. By default the value is 100 entries. The page size can never be larger than 250 entries. Optional.", |
|
1241 |
+ "format": "int32", |
|
1242 |
+ "minimum": "1", |
|
1243 |
+ "location": "query" |
|
1244 |
+ }, |
|
1245 |
+ "pageToken": { |
|
1246 |
+ "type": "string", |
|
1247 |
+ "description": "Token specifying which result page to return. Optional.", |
|
1248 |
+ "location": "query" |
|
1249 |
+ }, |
|
1250 |
+ "showDeleted": { |
|
1251 |
+ "type": "boolean", |
|
1252 |
+ "description": "Whether to include deleted ACLs in the result. Deleted ACLs are represented by role equal to \"none\". Deleted ACLs will always be included if syncToken is provided. Optional. The default is False.", |
|
1253 |
+ "location": "query" |
|
1254 |
+ }, |
|
1255 |
+ "syncToken": { |
|
1256 |
+ "type": "string", |
|
1257 |
+ "description": "Token obtained from the nextSyncToken field returned on the last page of results from the previous list request. It makes the result of this list request contain only entries that have changed since then. All entries deleted since the previous list request will always be in the result set and it is not allowed to set showDeleted to False.\nIf the syncToken expires, the server will respond with a 410 GONE response code and the client should clear its storage and perform a full synchronization without any syncToken.\nLearn more about incremental synchronization.\nOptional. The default is to return all entries.", |
|
1258 |
+ "location": "query" |
|
1259 |
+ } |
|
1260 |
+ }, |
|
1261 |
+ "parameterOrder": [ |
|
1262 |
+ "calendarId" |
|
1263 |
+ ], |
|
1264 |
+ "response": { |
|
1265 |
+ "$ref": "Acl" |
|
1266 |
+ }, |
|
1267 |
+ "scopes": [ |
|
1268 |
+ "https://www.googleapis.com/auth/calendar" |
|
1269 |
+ ], |
|
1270 |
+ "supportsSubscription": true |
|
1271 |
+ }, |
|
1272 |
+ "patch": { |
|
1273 |
+ "id": "calendar.acl.patch", |
|
1274 |
+ "path": "calendars/{calendarId}/acl/{ruleId}", |
|
1275 |
+ "httpMethod": "PATCH", |
|
1276 |
+ "description": "Updates an access control rule. This method supports patch semantics.", |
|
1277 |
+ "parameters": { |
|
1278 |
+ "calendarId": { |
|
1279 |
+ "type": "string", |
|
1280 |
+ "description": "Calendar identifier.", |
|
1281 |
+ "required": true, |
|
1282 |
+ "location": "path" |
|
1283 |
+ }, |
|
1284 |
+ "ruleId": { |
|
1285 |
+ "type": "string", |
|
1286 |
+ "description": "ACL rule identifier.", |
|
1287 |
+ "required": true, |
|
1288 |
+ "location": "path" |
|
1289 |
+ } |
|
1290 |
+ }, |
|
1291 |
+ "parameterOrder": [ |
|
1292 |
+ "calendarId", |
|
1293 |
+ "ruleId" |
|
1294 |
+ ], |
|
1295 |
+ "request": { |
|
1296 |
+ "$ref": "AclRule" |
|
1297 |
+ }, |
|
1298 |
+ "response": { |
|
1299 |
+ "$ref": "AclRule" |
|
1300 |
+ }, |
|
1301 |
+ "scopes": [ |
|
1302 |
+ "https://www.googleapis.com/auth/calendar" |
|
1303 |
+ ] |
|
1304 |
+ }, |
|
1305 |
+ "update": { |
|
1306 |
+ "id": "calendar.acl.update", |
|
1307 |
+ "path": "calendars/{calendarId}/acl/{ruleId}", |
|
1308 |
+ "httpMethod": "PUT", |
|
1309 |
+ "description": "Updates an access control rule.", |
|
1310 |
+ "parameters": { |
|
1311 |
+ "calendarId": { |
|
1312 |
+ "type": "string", |
|
1313 |
+ "description": "Calendar identifier.", |
|
1314 |
+ "required": true, |
|
1315 |
+ "location": "path" |
|
1316 |
+ }, |
|
1317 |
+ "ruleId": { |
|
1318 |
+ "type": "string", |
|
1319 |
+ "description": "ACL rule identifier.", |
|
1320 |
+ "required": true, |
|
1321 |
+ "location": "path" |
|
1322 |
+ } |
|
1323 |
+ }, |
|
1324 |
+ "parameterOrder": [ |
|
1325 |
+ "calendarId", |
|
1326 |
+ "ruleId" |
|
1327 |
+ ], |
|
1328 |
+ "request": { |
|
1329 |
+ "$ref": "AclRule" |
|
1330 |
+ }, |
|
1331 |
+ "response": { |
|
1332 |
+ "$ref": "AclRule" |
|
1333 |
+ }, |
|
1334 |
+ "scopes": [ |
|
1335 |
+ "https://www.googleapis.com/auth/calendar" |
|
1336 |
+ ] |
|
1337 |
+ }, |
|
1338 |
+ "watch": { |
|
1339 |
+ "id": "calendar.acl.watch", |
|
1340 |
+ "path": "calendars/{calendarId}/acl/watch", |
|
1341 |
+ "httpMethod": "POST", |
|
1342 |
+ "description": "Watch for changes to ACL resources.", |
|
1343 |
+ "parameters": { |
|
1344 |
+ "calendarId": { |
|
1345 |
+ "type": "string", |
|
1346 |
+ "description": "Calendar identifier.", |
|
1347 |
+ "required": true, |
|
1348 |
+ "location": "path" |
|
1349 |
+ }, |
|
1350 |
+ "maxResults": { |
|
1351 |
+ "type": "integer", |
|
1352 |
+ "description": "Maximum number of entries returned on one result page. By default the value is 100 entries. The page size can never be larger than 250 entries. Optional.", |
|
1353 |
+ "format": "int32", |
|
1354 |
+ "minimum": "1", |
|
1355 |
+ "location": "query" |
|
1356 |
+ }, |
|
1357 |
+ "pageToken": { |
|
1358 |
+ "type": "string", |
|
1359 |
+ "description": "Token specifying which result page to return. Optional.", |
|
1360 |
+ "location": "query" |
|
1361 |
+ }, |
|
1362 |
+ "showDeleted": { |
|
1363 |
+ "type": "boolean", |
|
1364 |
+ "description": "Whether to include deleted ACLs in the result. Deleted ACLs are represented by role equal to \"none\". Deleted ACLs will always be included if syncToken is provided. Optional. The default is False.", |
|
1365 |
+ "location": "query" |
|
1366 |
+ }, |
|
1367 |
+ "syncToken": { |
|
1368 |
+ "type": "string", |
|
1369 |
+ "description": "Token obtained from the nextSyncToken field returned on the last page of results from the previous list request. It makes the result of this list request contain only entries that have changed since then. All entries deleted since the previous list request will always be in the result set and it is not allowed to set showDeleted to False.\nIf the syncToken expires, the server will respond with a 410 GONE response code and the client should clear its storage and perform a full synchronization without any syncToken.\nLearn more about incremental synchronization.\nOptional. The default is to return all entries.", |
|
1370 |
+ "location": "query" |
|
1371 |
+ } |
|
1372 |
+ }, |
|
1373 |
+ "parameterOrder": [ |
|
1374 |
+ "calendarId" |
|
1375 |
+ ], |
|
1376 |
+ "request": { |
|
1377 |
+ "$ref": "Channel", |
|
1378 |
+ "parameterName": "resource" |
|
1379 |
+ }, |
|
1380 |
+ "response": { |
|
1381 |
+ "$ref": "Channel" |
|
1382 |
+ }, |
|
1383 |
+ "scopes": [ |
|
1384 |
+ "https://www.googleapis.com/auth/calendar" |
|
1385 |
+ ], |
|
1386 |
+ "supportsSubscription": true |
|
1387 |
+ } |
|
1388 |
+ } |
|
1389 |
+ }, |
|
1390 |
+ "calendarList": { |
|
1391 |
+ "methods": { |
|
1392 |
+ "delete": { |
|
1393 |
+ "id": "calendar.calendarList.delete", |
|
1394 |
+ "path": "users/me/calendarList/{calendarId}", |
|
1395 |
+ "httpMethod": "DELETE", |
|
1396 |
+ "description": "Deletes an entry on the user's calendar list.", |
|
1397 |
+ "parameters": { |
|
1398 |
+ "calendarId": { |
|
1399 |
+ "type": "string", |
|
1400 |
+ "description": "Calendar identifier.", |
|
1401 |
+ "required": true, |
|
1402 |
+ "location": "path" |
|
1403 |
+ } |
|
1404 |
+ }, |
|
1405 |
+ "parameterOrder": [ |
|
1406 |
+ "calendarId" |
|
1407 |
+ ], |
|
1408 |
+ "scopes": [ |
|
1409 |
+ "https://www.googleapis.com/auth/calendar" |
|
1410 |
+ ] |
|
1411 |
+ }, |
|
1412 |
+ "get": { |
|
1413 |
+ "id": "calendar.calendarList.get", |
|
1414 |
+ "path": "users/me/calendarList/{calendarId}", |
|
1415 |
+ "httpMethod": "GET", |
|
1416 |
+ "description": "Returns an entry on the user's calendar list.", |
|
1417 |
+ "parameters": { |
|
1418 |
+ "calendarId": { |
|
1419 |
+ "type": "string", |
|
1420 |
+ "description": "Calendar identifier.", |
|
1421 |
+ "required": true, |
|
1422 |
+ "location": "path" |
|
1423 |
+ } |
|
1424 |
+ }, |
|
1425 |
+ "parameterOrder": [ |
|
1426 |
+ "calendarId" |
|
1427 |
+ ], |
|
1428 |
+ "response": { |
|
1429 |
+ "$ref": "CalendarListEntry" |
|
1430 |
+ }, |
|
1431 |
+ "scopes": [ |
|
1432 |
+ "https://www.googleapis.com/auth/calendar", |
|
1433 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
1434 |
+ ] |
|
1435 |
+ }, |
|
1436 |
+ "insert": { |
|
1437 |
+ "id": "calendar.calendarList.insert", |
|
1438 |
+ "path": "users/me/calendarList", |
|
1439 |
+ "httpMethod": "POST", |
|
1440 |
+ "description": "Adds an entry to the user's calendar list.", |
|
1441 |
+ "parameters": { |
|
1442 |
+ "colorRgbFormat": { |
|
1443 |
+ "type": "boolean", |
|
1444 |
+ "description": "Whether to use the foregroundColor and backgroundColor fields to write the calendar colors (RGB). If this feature is used, the index-based colorId field will be set to the best matching option automatically. Optional. The default is False.", |
|
1445 |
+ "location": "query" |
|
1446 |
+ } |
|
1447 |
+ }, |
|
1448 |
+ "request": { |
|
1449 |
+ "$ref": "CalendarListEntry" |
|
1450 |
+ }, |
|
1451 |
+ "response": { |
|
1452 |
+ "$ref": "CalendarListEntry" |
|
1453 |
+ }, |
|
1454 |
+ "scopes": [ |
|
1455 |
+ "https://www.googleapis.com/auth/calendar" |
|
1456 |
+ ] |
|
1457 |
+ }, |
|
1458 |
+ "list": { |
|
1459 |
+ "id": "calendar.calendarList.list", |
|
1460 |
+ "path": "users/me/calendarList", |
|
1461 |
+ "httpMethod": "GET", |
|
1462 |
+ "description": "Returns entries on the user's calendar list.", |
|
1463 |
+ "parameters": { |
|
1464 |
+ "maxResults": { |
|
1465 |
+ "type": "integer", |
|
1466 |
+ "description": "Maximum number of entries returned on one result page. By default the value is 100 entries. The page size can never be larger than 250 entries. Optional.", |
|
1467 |
+ "format": "int32", |
|
1468 |
+ "minimum": "1", |
|
1469 |
+ "location": "query" |
|
1470 |
+ }, |
|
1471 |
+ "minAccessRole": { |
|
1472 |
+ "type": "string", |
|
1473 |
+ "description": "The minimum access role for the user in the returned entires. Optional. The default is no restriction.", |
|
1474 |
+ "enum": [ |
|
1475 |
+ "freeBusyReader", |
|
1476 |
+ "owner", |
|
1477 |
+ "reader", |
|
1478 |
+ "writer" |
|
1479 |
+ ], |
|
1480 |
+ "enumDescriptions": [ |
|
1481 |
+ "The user can read free/busy information.", |
|
1482 |
+ "The user can read and modify events and access control lists.", |
|
1483 |
+ "The user can read events that are not private.", |
|
1484 |
+ "The user can read and modify events." |
|
1485 |
+ ], |
|
1486 |
+ "location": "query" |
|
1487 |
+ }, |
|
1488 |
+ "pageToken": { |
|
1489 |
+ "type": "string", |
|
1490 |
+ "description": "Token specifying which result page to return. Optional.", |
|
1491 |
+ "location": "query" |
|
1492 |
+ }, |
|
1493 |
+ "showDeleted": { |
|
1494 |
+ "type": "boolean", |
|
1495 |
+ "description": "Whether to include deleted calendar list entries in the result. Optional. The default is False.", |
|
1496 |
+ "location": "query" |
|
1497 |
+ }, |
|
1498 |
+ "showHidden": { |
|
1499 |
+ "type": "boolean", |
|
1500 |
+ "description": "Whether to show hidden entries. Optional. The default is False.", |
|
1501 |
+ "location": "query" |
|
1502 |
+ }, |
|
1503 |
+ "syncToken": { |
|
1504 |
+ "type": "string", |
|
1505 |
+ "description": "Token obtained from the nextSyncToken field returned on the last page of results from the previous list request. It makes the result of this list request contain only entries that have changed since then. If only read-only fields such as calendar properties or ACLs have changed, the entry won't be returned. All entries deleted and hidden since the previous list request will always be in the result set and it is not allowed to set showDeleted neither showHidden to False.\nTo ensure client state consistency minAccessRole query parameter cannot be specified together with nextSyncToken.\nIf the syncToken expires, the server will respond with a 410 GONE response code and the client should clear its storage and perform a full synchronization without any syncToken.\nLearn more about incremental synchronization.\nOptional. The default is to return all entries.", |
|
1506 |
+ "location": "query" |
|
1507 |
+ } |
|
1508 |
+ }, |
|
1509 |
+ "response": { |
|
1510 |
+ "$ref": "CalendarList" |
|
1511 |
+ }, |
|
1512 |
+ "scopes": [ |
|
1513 |
+ "https://www.googleapis.com/auth/calendar", |
|
1514 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
1515 |
+ ], |
|
1516 |
+ "supportsSubscription": true |
|
1517 |
+ }, |
|
1518 |
+ "patch": { |
|
1519 |
+ "id": "calendar.calendarList.patch", |
|
1520 |
+ "path": "users/me/calendarList/{calendarId}", |
|
1521 |
+ "httpMethod": "PATCH", |
|
1522 |
+ "description": "Updates an entry on the user's calendar list. This method supports patch semantics.", |
|
1523 |
+ "parameters": { |
|
1524 |
+ "calendarId": { |
|
1525 |
+ "type": "string", |
|
1526 |
+ "description": "Calendar identifier.", |
|
1527 |
+ "required": true, |
|
1528 |
+ "location": "path" |
|
1529 |
+ }, |
|
1530 |
+ "colorRgbFormat": { |
|
1531 |
+ "type": "boolean", |
|
1532 |
+ "description": "Whether to use the foregroundColor and backgroundColor fields to write the calendar colors (RGB). If this feature is used, the index-based colorId field will be set to the best matching option automatically. Optional. The default is False.", |
|
1533 |
+ "location": "query" |
|
1534 |
+ } |
|
1535 |
+ }, |
|
1536 |
+ "parameterOrder": [ |
|
1537 |
+ "calendarId" |
|
1538 |
+ ], |
|
1539 |
+ "request": { |
|
1540 |
+ "$ref": "CalendarListEntry" |
|
1541 |
+ }, |
|
1542 |
+ "response": { |
|
1543 |
+ "$ref": "CalendarListEntry" |
|
1544 |
+ }, |
|
1545 |
+ "scopes": [ |
|
1546 |
+ "https://www.googleapis.com/auth/calendar" |
|
1547 |
+ ] |
|
1548 |
+ }, |
|
1549 |
+ "update": { |
|
1550 |
+ "id": "calendar.calendarList.update", |
|
1551 |
+ "path": "users/me/calendarList/{calendarId}", |
|
1552 |
+ "httpMethod": "PUT", |
|
1553 |
+ "description": "Updates an entry on the user's calendar list.", |
|
1554 |
+ "parameters": { |
|
1555 |
+ "calendarId": { |
|
1556 |
+ "type": "string", |
|
1557 |
+ "description": "Calendar identifier.", |
|
1558 |
+ "required": true, |
|
1559 |
+ "location": "path" |
|
1560 |
+ }, |
|
1561 |
+ "colorRgbFormat": { |
|
1562 |
+ "type": "boolean", |
|
1563 |
+ "description": "Whether to use the foregroundColor and backgroundColor fields to write the calendar colors (RGB). If this feature is used, the index-based colorId field will be set to the best matching option automatically. Optional. The default is False.", |
|
1564 |
+ "location": "query" |
|
1565 |
+ } |
|
1566 |
+ }, |
|
1567 |
+ "parameterOrder": [ |
|
1568 |
+ "calendarId" |
|
1569 |
+ ], |
|
1570 |
+ "request": { |
|
1571 |
+ "$ref": "CalendarListEntry" |
|
1572 |
+ }, |
|
1573 |
+ "response": { |
|
1574 |
+ "$ref": "CalendarListEntry" |
|
1575 |
+ }, |
|
1576 |
+ "scopes": [ |
|
1577 |
+ "https://www.googleapis.com/auth/calendar" |
|
1578 |
+ ] |
|
1579 |
+ }, |
|
1580 |
+ "watch": { |
|
1581 |
+ "id": "calendar.calendarList.watch", |
|
1582 |
+ "path": "users/me/calendarList/watch", |
|
1583 |
+ "httpMethod": "POST", |
|
1584 |
+ "description": "Watch for changes to CalendarList resources.", |
|
1585 |
+ "parameters": { |
|
1586 |
+ "maxResults": { |
|
1587 |
+ "type": "integer", |
|
1588 |
+ "description": "Maximum number of entries returned on one result page. By default the value is 100 entries. The page size can never be larger than 250 entries. Optional.", |
|
1589 |
+ "format": "int32", |
|
1590 |
+ "minimum": "1", |
|
1591 |
+ "location": "query" |
|
1592 |
+ }, |
|
1593 |
+ "minAccessRole": { |
|
1594 |
+ "type": "string", |
|
1595 |
+ "description": "The minimum access role for the user in the returned entires. Optional. The default is no restriction.", |
|
1596 |
+ "enum": [ |
|
1597 |
+ "freeBusyReader", |
|
1598 |
+ "owner", |
|
1599 |
+ "reader", |
|
1600 |
+ "writer" |
|
1601 |
+ ], |
|
1602 |
+ "enumDescriptions": [ |
|
1603 |
+ "The user can read free/busy information.", |
|
1604 |
+ "The user can read and modify events and access control lists.", |
|
1605 |
+ "The user can read events that are not private.", |
|
1606 |
+ "The user can read and modify events." |
|
1607 |
+ ], |
|
1608 |
+ "location": "query" |
|
1609 |
+ }, |
|
1610 |
+ "pageToken": { |
|
1611 |
+ "type": "string", |
|
1612 |
+ "description": "Token specifying which result page to return. Optional.", |
|
1613 |
+ "location": "query" |
|
1614 |
+ }, |
|
1615 |
+ "showDeleted": { |
|
1616 |
+ "type": "boolean", |
|
1617 |
+ "description": "Whether to include deleted calendar list entries in the result. Optional. The default is False.", |
|
1618 |
+ "location": "query" |
|
1619 |
+ }, |
|
1620 |
+ "showHidden": { |
|
1621 |
+ "type": "boolean", |
|
1622 |
+ "description": "Whether to show hidden entries. Optional. The default is False.", |
|
1623 |
+ "location": "query" |
|
1624 |
+ }, |
|
1625 |
+ "syncToken": { |
|
1626 |
+ "type": "string", |
|
1627 |
+ "description": "Token obtained from the nextSyncToken field returned on the last page of results from the previous list request. It makes the result of this list request contain only entries that have changed since then. If only read-only fields such as calendar properties or ACLs have changed, the entry won't be returned. All entries deleted and hidden since the previous list request will always be in the result set and it is not allowed to set showDeleted neither showHidden to False.\nTo ensure client state consistency minAccessRole query parameter cannot be specified together with nextSyncToken.\nIf the syncToken expires, the server will respond with a 410 GONE response code and the client should clear its storage and perform a full synchronization without any syncToken.\nLearn more about incremental synchronization.\nOptional. The default is to return all entries.", |
|
1628 |
+ "location": "query" |
|
1629 |
+ } |
|
1630 |
+ }, |
|
1631 |
+ "request": { |
|
1632 |
+ "$ref": "Channel", |
|
1633 |
+ "parameterName": "resource" |
|
1634 |
+ }, |
|
1635 |
+ "response": { |
|
1636 |
+ "$ref": "Channel" |
|
1637 |
+ }, |
|
1638 |
+ "scopes": [ |
|
1639 |
+ "https://www.googleapis.com/auth/calendar", |
|
1640 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
1641 |
+ ], |
|
1642 |
+ "supportsSubscription": true |
|
1643 |
+ } |
|
1644 |
+ } |
|
1645 |
+ }, |
|
1646 |
+ "calendars": { |
|
1647 |
+ "methods": { |
|
1648 |
+ "clear": { |
|
1649 |
+ "id": "calendar.calendars.clear", |
|
1650 |
+ "path": "calendars/{calendarId}/clear", |
|
1651 |
+ "httpMethod": "POST", |
|
1652 |
+ "description": "Clears a primary calendar. This operation deletes all data associated with the primary calendar of an account and cannot be undone.", |
|
1653 |
+ "parameters": { |
|
1654 |
+ "calendarId": { |
|
1655 |
+ "type": "string", |
|
1656 |
+ "description": "Calendar identifier.", |
|
1657 |
+ "required": true, |
|
1658 |
+ "location": "path" |
|
1659 |
+ } |
|
1660 |
+ }, |
|
1661 |
+ "parameterOrder": [ |
|
1662 |
+ "calendarId" |
|
1663 |
+ ], |
|
1664 |
+ "scopes": [ |
|
1665 |
+ "https://www.googleapis.com/auth/calendar" |
|
1666 |
+ ] |
|
1667 |
+ }, |
|
1668 |
+ "delete": { |
|
1669 |
+ "id": "calendar.calendars.delete", |
|
1670 |
+ "path": "calendars/{calendarId}", |
|
1671 |
+ "httpMethod": "DELETE", |
|
1672 |
+ "description": "Deletes a secondary calendar.", |
|
1673 |
+ "parameters": { |
|
1674 |
+ "calendarId": { |
|
1675 |
+ "type": "string", |
|
1676 |
+ "description": "Calendar identifier.", |
|
1677 |
+ "required": true, |
|
1678 |
+ "location": "path" |
|
1679 |
+ } |
|
1680 |
+ }, |
|
1681 |
+ "parameterOrder": [ |
|
1682 |
+ "calendarId" |
|
1683 |
+ ], |
|
1684 |
+ "scopes": [ |
|
1685 |
+ "https://www.googleapis.com/auth/calendar" |
|
1686 |
+ ] |
|
1687 |
+ }, |
|
1688 |
+ "get": { |
|
1689 |
+ "id": "calendar.calendars.get", |
|
1690 |
+ "path": "calendars/{calendarId}", |
|
1691 |
+ "httpMethod": "GET", |
|
1692 |
+ "description": "Returns metadata for a calendar.", |
|
1693 |
+ "parameters": { |
|
1694 |
+ "calendarId": { |
|
1695 |
+ "type": "string", |
|
1696 |
+ "description": "Calendar identifier.", |
|
1697 |
+ "required": true, |
|
1698 |
+ "location": "path" |
|
1699 |
+ } |
|
1700 |
+ }, |
|
1701 |
+ "parameterOrder": [ |
|
1702 |
+ "calendarId" |
|
1703 |
+ ], |
|
1704 |
+ "response": { |
|
1705 |
+ "$ref": "Calendar" |
|
1706 |
+ }, |
|
1707 |
+ "scopes": [ |
|
1708 |
+ "https://www.googleapis.com/auth/calendar", |
|
1709 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
1710 |
+ ] |
|
1711 |
+ }, |
|
1712 |
+ "insert": { |
|
1713 |
+ "id": "calendar.calendars.insert", |
|
1714 |
+ "path": "calendars", |
|
1715 |
+ "httpMethod": "POST", |
|
1716 |
+ "description": "Creates a secondary calendar.", |
|
1717 |
+ "request": { |
|
1718 |
+ "$ref": "Calendar" |
|
1719 |
+ }, |
|
1720 |
+ "response": { |
|
1721 |
+ "$ref": "Calendar" |
|
1722 |
+ }, |
|
1723 |
+ "scopes": [ |
|
1724 |
+ "https://www.googleapis.com/auth/calendar" |
|
1725 |
+ ] |
|
1726 |
+ }, |
|
1727 |
+ "patch": { |
|
1728 |
+ "id": "calendar.calendars.patch", |
|
1729 |
+ "path": "calendars/{calendarId}", |
|
1730 |
+ "httpMethod": "PATCH", |
|
1731 |
+ "description": "Updates metadata for a calendar. This method supports patch semantics.", |
|
1732 |
+ "parameters": { |
|
1733 |
+ "calendarId": { |
|
1734 |
+ "type": "string", |
|
1735 |
+ "description": "Calendar identifier.", |
|
1736 |
+ "required": true, |
|
1737 |
+ "location": "path" |
|
1738 |
+ } |
|
1739 |
+ }, |
|
1740 |
+ "parameterOrder": [ |
|
1741 |
+ "calendarId" |
|
1742 |
+ ], |
|
1743 |
+ "request": { |
|
1744 |
+ "$ref": "Calendar" |
|
1745 |
+ }, |
|
1746 |
+ "response": { |
|
1747 |
+ "$ref": "Calendar" |
|
1748 |
+ }, |
|
1749 |
+ "scopes": [ |
|
1750 |
+ "https://www.googleapis.com/auth/calendar" |
|
1751 |
+ ] |
|
1752 |
+ }, |
|
1753 |
+ "update": { |
|
1754 |
+ "id": "calendar.calendars.update", |
|
1755 |
+ "path": "calendars/{calendarId}", |
|
1756 |
+ "httpMethod": "PUT", |
|
1757 |
+ "description": "Updates metadata for a calendar.", |
|
1758 |
+ "parameters": { |
|
1759 |
+ "calendarId": { |
|
1760 |
+ "type": "string", |
|
1761 |
+ "description": "Calendar identifier.", |
|
1762 |
+ "required": true, |
|
1763 |
+ "location": "path" |
|
1764 |
+ } |
|
1765 |
+ }, |
|
1766 |
+ "parameterOrder": [ |
|
1767 |
+ "calendarId" |
|
1768 |
+ ], |
|
1769 |
+ "request": { |
|
1770 |
+ "$ref": "Calendar" |
|
1771 |
+ }, |
|
1772 |
+ "response": { |
|
1773 |
+ "$ref": "Calendar" |
|
1774 |
+ }, |
|
1775 |
+ "scopes": [ |
|
1776 |
+ "https://www.googleapis.com/auth/calendar" |
|
1777 |
+ ] |
|
1778 |
+ } |
|
1779 |
+ } |
|
1780 |
+ }, |
|
1781 |
+ "channels": { |
|
1782 |
+ "methods": { |
|
1783 |
+ "stop": { |
|
1784 |
+ "id": "calendar.channels.stop", |
|
1785 |
+ "path": "channels/stop", |
|
1786 |
+ "httpMethod": "POST", |
|
1787 |
+ "description": "Stop watching resources through this channel", |
|
1788 |
+ "request": { |
|
1789 |
+ "$ref": "Channel", |
|
1790 |
+ "parameterName": "resource" |
|
1791 |
+ }, |
|
1792 |
+ "scopes": [ |
|
1793 |
+ "https://www.googleapis.com/auth/calendar", |
|
1794 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
1795 |
+ ] |
|
1796 |
+ } |
|
1797 |
+ } |
|
1798 |
+ }, |
|
1799 |
+ "colors": { |
|
1800 |
+ "methods": { |
|
1801 |
+ "get": { |
|
1802 |
+ "id": "calendar.colors.get", |
|
1803 |
+ "path": "colors", |
|
1804 |
+ "httpMethod": "GET", |
|
1805 |
+ "description": "Returns the color definitions for calendars and events.", |
|
1806 |
+ "response": { |
|
1807 |
+ "$ref": "Colors" |
|
1808 |
+ }, |
|
1809 |
+ "scopes": [ |
|
1810 |
+ "https://www.googleapis.com/auth/calendar", |
|
1811 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
1812 |
+ ] |
|
1813 |
+ } |
|
1814 |
+ } |
|
1815 |
+ }, |
|
1816 |
+ "events": { |
|
1817 |
+ "methods": { |
|
1818 |
+ "delete": { |
|
1819 |
+ "id": "calendar.events.delete", |
|
1820 |
+ "path": "calendars/{calendarId}/events/{eventId}", |
|
1821 |
+ "httpMethod": "DELETE", |
|
1822 |
+ "description": "Deletes an event.", |
|
1823 |
+ "parameters": { |
|
1824 |
+ "calendarId": { |
|
1825 |
+ "type": "string", |
|
1826 |
+ "description": "Calendar identifier.", |
|
1827 |
+ "required": true, |
|
1828 |
+ "location": "path" |
|
1829 |
+ }, |
|
1830 |
+ "eventId": { |
|
1831 |
+ "type": "string", |
|
1832 |
+ "description": "Event identifier.", |
|
1833 |
+ "required": true, |
|
1834 |
+ "location": "path" |
|
1835 |
+ }, |
|
1836 |
+ "sendNotifications": { |
|
1837 |
+ "type": "boolean", |
|
1838 |
+ "description": "Whether to send notifications about the deletion of the event. Optional. The default is False.", |
|
1839 |
+ "location": "query" |
|
1840 |
+ } |
|
1841 |
+ }, |
|
1842 |
+ "parameterOrder": [ |
|
1843 |
+ "calendarId", |
|
1844 |
+ "eventId" |
|
1845 |
+ ], |
|
1846 |
+ "scopes": [ |
|
1847 |
+ "https://www.googleapis.com/auth/calendar" |
|
1848 |
+ ] |
|
1849 |
+ }, |
|
1850 |
+ "get": { |
|
1851 |
+ "id": "calendar.events.get", |
|
1852 |
+ "path": "calendars/{calendarId}/events/{eventId}", |
|
1853 |
+ "httpMethod": "GET", |
|
1854 |
+ "description": "Returns an event.", |
|
1855 |
+ "parameters": { |
|
1856 |
+ "alwaysIncludeEmail": { |
|
1857 |
+ "type": "boolean", |
|
1858 |
+ "description": "Whether to always include a value in the email field for the organizer, creator and attendees, even if no real email is available (i.e. a generated, non-working value will be provided). The use of this option is discouraged and should only be used by clients which cannot handle the absence of an email address value in the mentioned places. Optional. The default is False.", |
|
1859 |
+ "location": "query" |
|
1860 |
+ }, |
|
1861 |
+ "calendarId": { |
|
1862 |
+ "type": "string", |
|
1863 |
+ "description": "Calendar identifier.", |
|
1864 |
+ "required": true, |
|
1865 |
+ "location": "path" |
|
1866 |
+ }, |
|
1867 |
+ "eventId": { |
|
1868 |
+ "type": "string", |
|
1869 |
+ "description": "Event identifier.", |
|
1870 |
+ "required": true, |
|
1871 |
+ "location": "path" |
|
1872 |
+ }, |
|
1873 |
+ "maxAttendees": { |
|
1874 |
+ "type": "integer", |
|
1875 |
+ "description": "The maximum number of attendees to include in the response. If there are more than the specified number of attendees, only the participant is returned. Optional.", |
|
1876 |
+ "format": "int32", |
|
1877 |
+ "minimum": "1", |
|
1878 |
+ "location": "query" |
|
1879 |
+ }, |
|
1880 |
+ "timeZone": { |
|
1881 |
+ "type": "string", |
|
1882 |
+ "description": "Time zone used in the response. Optional. The default is the time zone of the calendar.", |
|
1883 |
+ "location": "query" |
|
1884 |
+ } |
|
1885 |
+ }, |
|
1886 |
+ "parameterOrder": [ |
|
1887 |
+ "calendarId", |
|
1888 |
+ "eventId" |
|
1889 |
+ ], |
|
1890 |
+ "response": { |
|
1891 |
+ "$ref": "Event" |
|
1892 |
+ }, |
|
1893 |
+ "scopes": [ |
|
1894 |
+ "https://www.googleapis.com/auth/calendar", |
|
1895 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
1896 |
+ ] |
|
1897 |
+ }, |
|
1898 |
+ "import": { |
|
1899 |
+ "id": "calendar.events.import", |
|
1900 |
+ "path": "calendars/{calendarId}/events/import", |
|
1901 |
+ "httpMethod": "POST", |
|
1902 |
+ "description": "Imports an event. This operation is used to add a private copy of an existing event to a calendar.", |
|
1903 |
+ "parameters": { |
|
1904 |
+ "calendarId": { |
|
1905 |
+ "type": "string", |
|
1906 |
+ "description": "Calendar identifier.", |
|
1907 |
+ "required": true, |
|
1908 |
+ "location": "path" |
|
1909 |
+ } |
|
1910 |
+ }, |
|
1911 |
+ "parameterOrder": [ |
|
1912 |
+ "calendarId" |
|
1913 |
+ ], |
|
1914 |
+ "request": { |
|
1915 |
+ "$ref": "Event" |
|
1916 |
+ }, |
|
1917 |
+ "response": { |
|
1918 |
+ "$ref": "Event" |
|
1919 |
+ }, |
|
1920 |
+ "scopes": [ |
|
1921 |
+ "https://www.googleapis.com/auth/calendar" |
|
1922 |
+ ] |
|
1923 |
+ }, |
|
1924 |
+ "insert": { |
|
1925 |
+ "id": "calendar.events.insert", |
|
1926 |
+ "path": "calendars/{calendarId}/events", |
|
1927 |
+ "httpMethod": "POST", |
|
1928 |
+ "description": "Creates an event.", |
|
1929 |
+ "parameters": { |
|
1930 |
+ "calendarId": { |
|
1931 |
+ "type": "string", |
|
1932 |
+ "description": "Calendar identifier.", |
|
1933 |
+ "required": true, |
|
1934 |
+ "location": "path" |
|
1935 |
+ }, |
|
1936 |
+ "maxAttendees": { |
|
1937 |
+ "type": "integer", |
|
1938 |
+ "description": "The maximum number of attendees to include in the response. If there are more than the specified number of attendees, only the participant is returned. Optional.", |
|
1939 |
+ "format": "int32", |
|
1940 |
+ "minimum": "1", |
|
1941 |
+ "location": "query" |
|
1942 |
+ }, |
|
1943 |
+ "sendNotifications": { |
|
1944 |
+ "type": "boolean", |
|
1945 |
+ "description": "Whether to send notifications about the creation of the new event. Optional. The default is False.", |
|
1946 |
+ "location": "query" |
|
1947 |
+ } |
|
1948 |
+ }, |
|
1949 |
+ "parameterOrder": [ |
|
1950 |
+ "calendarId" |
|
1951 |
+ ], |
|
1952 |
+ "request": { |
|
1953 |
+ "$ref": "Event" |
|
1954 |
+ }, |
|
1955 |
+ "response": { |
|
1956 |
+ "$ref": "Event" |
|
1957 |
+ }, |
|
1958 |
+ "scopes": [ |
|
1959 |
+ "https://www.googleapis.com/auth/calendar" |
|
1960 |
+ ] |
|
1961 |
+ }, |
|
1962 |
+ "instances": { |
|
1963 |
+ "id": "calendar.events.instances", |
|
1964 |
+ "path": "calendars/{calendarId}/events/{eventId}/instances", |
|
1965 |
+ "httpMethod": "GET", |
|
1966 |
+ "description": "Returns instances of the specified recurring event.", |
|
1967 |
+ "parameters": { |
|
1968 |
+ "alwaysIncludeEmail": { |
|
1969 |
+ "type": "boolean", |
|
1970 |
+ "description": "Whether to always include a value in the email field for the organizer, creator and attendees, even if no real email is available (i.e. a generated, non-working value will be provided). The use of this option is discouraged and should only be used by clients which cannot handle the absence of an email address value in the mentioned places. Optional. The default is False.", |
|
1971 |
+ "location": "query" |
|
1972 |
+ }, |
|
1973 |
+ "calendarId": { |
|
1974 |
+ "type": "string", |
|
1975 |
+ "description": "Calendar identifier.", |
|
1976 |
+ "required": true, |
|
1977 |
+ "location": "path" |
|
1978 |
+ }, |
|
1979 |
+ "eventId": { |
|
1980 |
+ "type": "string", |
|
1981 |
+ "description": "Recurring event identifier.", |
|
1982 |
+ "required": true, |
|
1983 |
+ "location": "path" |
|
1984 |
+ }, |
|
1985 |
+ "maxAttendees": { |
|
1986 |
+ "type": "integer", |
|
1987 |
+ "description": "The maximum number of attendees to include in the response. If there are more than the specified number of attendees, only the participant is returned. Optional.", |
|
1988 |
+ "format": "int32", |
|
1989 |
+ "minimum": "1", |
|
1990 |
+ "location": "query" |
|
1991 |
+ }, |
|
1992 |
+ "maxResults": { |
|
1993 |
+ "type": "integer", |
|
1994 |
+ "description": "Maximum number of events returned on one result page. By default the value is 250 events. The page size can never be larger than 2500 events. Optional.", |
|
1995 |
+ "format": "int32", |
|
1996 |
+ "minimum": "1", |
|
1997 |
+ "location": "query" |
|
1998 |
+ }, |
|
1999 |
+ "originalStart": { |
|
2000 |
+ "type": "string", |
|
2001 |
+ "description": "The original start time of the instance in the result. Optional.", |
|
2002 |
+ "location": "query" |
|
2003 |
+ }, |
|
2004 |
+ "pageToken": { |
|
2005 |
+ "type": "string", |
|
2006 |
+ "description": "Token specifying which result page to return. Optional.", |
|
2007 |
+ "location": "query" |
|
2008 |
+ }, |
|
2009 |
+ "showDeleted": { |
|
2010 |
+ "type": "boolean", |
|
2011 |
+ "description": "Whether to include deleted events (with status equals \"cancelled\") in the result. Cancelled instances of recurring events will still be included if singleEvents is False. Optional. The default is False.", |
|
2012 |
+ "location": "query" |
|
2013 |
+ }, |
|
2014 |
+ "timeMax": { |
|
2015 |
+ "type": "string", |
|
2016 |
+ "description": "Upper bound (exclusive) for an event's start time to filter by. Optional. The default is not to filter by start time.", |
|
2017 |
+ "format": "date-time", |
|
2018 |
+ "location": "query" |
|
2019 |
+ }, |
|
2020 |
+ "timeMin": { |
|
2021 |
+ "type": "string", |
|
2022 |
+ "description": "Lower bound (inclusive) for an event's end time to filter by. Optional. The default is not to filter by end time.", |
|
2023 |
+ "format": "date-time", |
|
2024 |
+ "location": "query" |
|
2025 |
+ }, |
|
2026 |
+ "timeZone": { |
|
2027 |
+ "type": "string", |
|
2028 |
+ "description": "Time zone used in the response. Optional. The default is the time zone of the calendar.", |
|
2029 |
+ "location": "query" |
|
2030 |
+ } |
|
2031 |
+ }, |
|
2032 |
+ "parameterOrder": [ |
|
2033 |
+ "calendarId", |
|
2034 |
+ "eventId" |
|
2035 |
+ ], |
|
2036 |
+ "response": { |
|
2037 |
+ "$ref": "Events" |
|
2038 |
+ }, |
|
2039 |
+ "scopes": [ |
|
2040 |
+ "https://www.googleapis.com/auth/calendar", |
|
2041 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
2042 |
+ ], |
|
2043 |
+ "supportsSubscription": true |
|
2044 |
+ }, |
|
2045 |
+ "list": { |
|
2046 |
+ "id": "calendar.events.list", |
|
2047 |
+ "path": "calendars/{calendarId}/events", |
|
2048 |
+ "httpMethod": "GET", |
|
2049 |
+ "description": "Returns events on the specified calendar.", |
|
2050 |
+ "parameters": { |
|
2051 |
+ "alwaysIncludeEmail": { |
|
2052 |
+ "type": "boolean", |
|
2053 |
+ "description": "Whether to always include a value in the email field for the organizer, creator and attendees, even if no real email is available (i.e. a generated, non-working value will be provided). The use of this option is discouraged and should only be used by clients which cannot handle the absence of an email address value in the mentioned places. Optional. The default is False.", |
|
2054 |
+ "location": "query" |
|
2055 |
+ }, |
|
2056 |
+ "calendarId": { |
|
2057 |
+ "type": "string", |
|
2058 |
+ "description": "Calendar identifier.", |
|
2059 |
+ "required": true, |
|
2060 |
+ "location": "path" |
|
2061 |
+ }, |
|
2062 |
+ "iCalUID": { |
|
2063 |
+ "type": "string", |
|
2064 |
+ "description": "Specifies event ID in the iCalendar format to be included in the response. Optional.", |
|
2065 |
+ "location": "query" |
|
2066 |
+ }, |
|
2067 |
+ "maxAttendees": { |
|
2068 |
+ "type": "integer", |
|
2069 |
+ "description": "The maximum number of attendees to include in the response. If there are more than the specified number of attendees, only the participant is returned. Optional.", |
|
2070 |
+ "format": "int32", |
|
2071 |
+ "minimum": "1", |
|
2072 |
+ "location": "query" |
|
2073 |
+ }, |
|
2074 |
+ "maxResults": { |
|
2075 |
+ "type": "integer", |
|
2076 |
+ "description": "Maximum number of events returned on one result page. By default the value is 250 events. The page size can never be larger than 2500 events. Optional.", |
|
2077 |
+ "format": "int32", |
|
2078 |
+ "minimum": "1", |
|
2079 |
+ "location": "query" |
|
2080 |
+ }, |
|
2081 |
+ "orderBy": { |
|
2082 |
+ "type": "string", |
|
2083 |
+ "description": "The order of the events returned in the result. Optional. The default is an unspecified, stable order.", |
|
2084 |
+ "enum": [ |
|
2085 |
+ "startTime", |
|
2086 |
+ "updated" |
|
2087 |
+ ], |
|
2088 |
+ "enumDescriptions": [ |
|
2089 |
+ "Order by the start date/time (ascending). This is only available when querying single events (i.e. the parameter singleEvents is True)", |
|
2090 |
+ "Order by last modification time (ascending)." |
|
2091 |
+ ], |
|
2092 |
+ "location": "query" |
|
2093 |
+ }, |
|
2094 |
+ "pageToken": { |
|
2095 |
+ "type": "string", |
|
2096 |
+ "description": "Token specifying which result page to return. Optional.", |
|
2097 |
+ "location": "query" |
|
2098 |
+ }, |
|
2099 |
+ "privateExtendedProperty": { |
|
2100 |
+ "type": "string", |
|
2101 |
+ "description": "Extended properties constraint specified as propertyName=value. Matches only private properties. This parameter might be repeated multiple times to return events that match all given constraints.", |
|
2102 |
+ "repeated": true, |
|
2103 |
+ "location": "query" |
|
2104 |
+ }, |
|
2105 |
+ "q": { |
|
2106 |
+ "type": "string", |
|
2107 |
+ "description": "Free text search terms to find events that match these terms in any field, except for extended properties. Optional.", |
|
2108 |
+ "location": "query" |
|
2109 |
+ }, |
|
2110 |
+ "sharedExtendedProperty": { |
|
2111 |
+ "type": "string", |
|
2112 |
+ "description": "Extended properties constraint specified as propertyName=value. Matches only shared properties. This parameter might be repeated multiple times to return events that match all given constraints.", |
|
2113 |
+ "repeated": true, |
|
2114 |
+ "location": "query" |
|
2115 |
+ }, |
|
2116 |
+ "showDeleted": { |
|
2117 |
+ "type": "boolean", |
|
2118 |
+ "description": "Whether to include deleted events (with status equals \"cancelled\") in the result. Cancelled instances of recurring events (but not the underlying recurring event) will still be included if showDeleted and singleEvents are both False. If showDeleted and singleEvents are both True, only single instances of deleted events (but not the underlying recurring events) are returned. Optional. The default is False.", |
|
2119 |
+ "location": "query" |
|
2120 |
+ }, |
|
2121 |
+ "showHiddenInvitations": { |
|
2122 |
+ "type": "boolean", |
|
2123 |
+ "description": "Whether to include hidden invitations in the result. Optional. The default is False.", |
|
2124 |
+ "location": "query" |
|
2125 |
+ }, |
|
2126 |
+ "singleEvents": { |
|
2127 |
+ "type": "boolean", |
|
2128 |
+ "description": "Whether to expand recurring events into instances and only return single one-off events and instances of recurring events, but not the underlying recurring events themselves. Optional. The default is False.", |
|
2129 |
+ "location": "query" |
|
2130 |
+ }, |
|
2131 |
+ "syncToken": { |
|
2132 |
+ "type": "string", |
|
2133 |
+ "description": "Token obtained from the nextSyncToken field returned on the last page of results from the previous list request. It makes the result of this list request contain only entries that have changed since then. All events deleted since the previous list request will always be in the result set and it is not allowed to set showDeleted to False.\nThere are several query parameters that cannot be specified together with nextSyncToken to ensure consistency of the client state.\n\nThese are: \n- iCalUID \n- orderBy \n- privateExtendedProperty \n- q \n- sharedExtendedProperty \n- timeMin \n- timeMax \n- updatedMin If the syncToken expires, the server will respond with a 410 GONE response code and the client should clear its storage and perform a full synchronization without any syncToken.\nLearn more about incremental synchronization.\nOptional. The default is to return all entries.", |
|
2134 |
+ "location": "query" |
|
2135 |
+ }, |
|
2136 |
+ "timeMax": { |
|
2137 |
+ "type": "string", |
|
2138 |
+ "description": "Upper bound (exclusive) for an event's start time to filter by. Optional. The default is not to filter by start time.", |
|
2139 |
+ "format": "date-time", |
|
2140 |
+ "location": "query" |
|
2141 |
+ }, |
|
2142 |
+ "timeMin": { |
|
2143 |
+ "type": "string", |
|
2144 |
+ "description": "Lower bound (inclusive) for an event's end time to filter by. Optional. The default is not to filter by end time.", |
|
2145 |
+ "format": "date-time", |
|
2146 |
+ "location": "query" |
|
2147 |
+ }, |
|
2148 |
+ "timeZone": { |
|
2149 |
+ "type": "string", |
|
2150 |
+ "description": "Time zone used in the response. Optional. The default is the time zone of the calendar.", |
|
2151 |
+ "location": "query" |
|
2152 |
+ }, |
|
2153 |
+ "updatedMin": { |
|
2154 |
+ "type": "string", |
|
2155 |
+ "description": "Lower bound for an event's last modification time (as a RFC 3339 timestamp) to filter by. When specified, entries deleted since this time will always be included regardless of showDeleted. Optional. The default is not to filter by last modification time.", |
|
2156 |
+ "format": "date-time", |
|
2157 |
+ "location": "query" |
|
2158 |
+ } |
|
2159 |
+ }, |
|
2160 |
+ "parameterOrder": [ |
|
2161 |
+ "calendarId" |
|
2162 |
+ ], |
|
2163 |
+ "response": { |
|
2164 |
+ "$ref": "Events" |
|
2165 |
+ }, |
|
2166 |
+ "scopes": [ |
|
2167 |
+ "https://www.googleapis.com/auth/calendar", |
|
2168 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
2169 |
+ ], |
|
2170 |
+ "supportsSubscription": true |
|
2171 |
+ }, |
|
2172 |
+ "move": { |
|
2173 |
+ "id": "calendar.events.move", |
|
2174 |
+ "path": "calendars/{calendarId}/events/{eventId}/move", |
|
2175 |
+ "httpMethod": "POST", |
|
2176 |
+ "description": "Moves an event to another calendar, i.e. changes an event's organizer.", |
|
2177 |
+ "parameters": { |
|
2178 |
+ "calendarId": { |
|
2179 |
+ "type": "string", |
|
2180 |
+ "description": "Calendar identifier of the source calendar where the event currently is on.", |
|
2181 |
+ "required": true, |
|
2182 |
+ "location": "path" |
|
2183 |
+ }, |
|
2184 |
+ "destination": { |
|
2185 |
+ "type": "string", |
|
2186 |
+ "description": "Calendar identifier of the target calendar where the event is to be moved to.", |
|
2187 |
+ "required": true, |
|
2188 |
+ "location": "query" |
|
2189 |
+ }, |
|
2190 |
+ "eventId": { |
|
2191 |
+ "type": "string", |
|
2192 |
+ "description": "Event identifier.", |
|
2193 |
+ "required": true, |
|
2194 |
+ "location": "path" |
|
2195 |
+ }, |
|
2196 |
+ "sendNotifications": { |
|
2197 |
+ "type": "boolean", |
|
2198 |
+ "description": "Whether to send notifications about the change of the event's organizer. Optional. The default is False.", |
|
2199 |
+ "location": "query" |
|
2200 |
+ } |
|
2201 |
+ }, |
|
2202 |
+ "parameterOrder": [ |
|
2203 |
+ "calendarId", |
|
2204 |
+ "eventId", |
|
2205 |
+ "destination" |
|
2206 |
+ ], |
|
2207 |
+ "response": { |
|
2208 |
+ "$ref": "Event" |
|
2209 |
+ }, |
|
2210 |
+ "scopes": [ |
|
2211 |
+ "https://www.googleapis.com/auth/calendar" |
|
2212 |
+ ] |
|
2213 |
+ }, |
|
2214 |
+ "patch": { |
|
2215 |
+ "id": "calendar.events.patch", |
|
2216 |
+ "path": "calendars/{calendarId}/events/{eventId}", |
|
2217 |
+ "httpMethod": "PATCH", |
|
2218 |
+ "description": "Updates an event. This method supports patch semantics.", |
|
2219 |
+ "parameters": { |
|
2220 |
+ "alwaysIncludeEmail": { |
|
2221 |
+ "type": "boolean", |
|
2222 |
+ "description": "Whether to always include a value in the email field for the organizer, creator and attendees, even if no real email is available (i.e. a generated, non-working value will be provided). The use of this option is discouraged and should only be used by clients which cannot handle the absence of an email address value in the mentioned places. Optional. The default is False.", |
|
2223 |
+ "location": "query" |
|
2224 |
+ }, |
|
2225 |
+ "calendarId": { |
|
2226 |
+ "type": "string", |
|
2227 |
+ "description": "Calendar identifier.", |
|
2228 |
+ "required": true, |
|
2229 |
+ "location": "path" |
|
2230 |
+ }, |
|
2231 |
+ "eventId": { |
|
2232 |
+ "type": "string", |
|
2233 |
+ "description": "Event identifier.", |
|
2234 |
+ "required": true, |
|
2235 |
+ "location": "path" |
|
2236 |
+ }, |
|
2237 |
+ "maxAttendees": { |
|
2238 |
+ "type": "integer", |
|
2239 |
+ "description": "The maximum number of attendees to include in the response. If there are more than the specified number of attendees, only the participant is returned. Optional.", |
|
2240 |
+ "format": "int32", |
|
2241 |
+ "minimum": "1", |
|
2242 |
+ "location": "query" |
|
2243 |
+ }, |
|
2244 |
+ "sendNotifications": { |
|
2245 |
+ "type": "boolean", |
|
2246 |
+ "description": "Whether to send notifications about the event update (e.g. attendee's responses, title changes, etc.). Optional. The default is False.", |
|
2247 |
+ "location": "query" |
|
2248 |
+ } |
|
2249 |
+ }, |
|
2250 |
+ "parameterOrder": [ |
|
2251 |
+ "calendarId", |
|
2252 |
+ "eventId" |
|
2253 |
+ ], |
|
2254 |
+ "request": { |
|
2255 |
+ "$ref": "Event" |
|
2256 |
+ }, |
|
2257 |
+ "response": { |
|
2258 |
+ "$ref": "Event" |
|
2259 |
+ }, |
|
2260 |
+ "scopes": [ |
|
2261 |
+ "https://www.googleapis.com/auth/calendar" |
|
2262 |
+ ] |
|
2263 |
+ }, |
|
2264 |
+ "quickAdd": { |
|
2265 |
+ "id": "calendar.events.quickAdd", |
|
2266 |
+ "path": "calendars/{calendarId}/events/quickAdd", |
|
2267 |
+ "httpMethod": "POST", |
|
2268 |
+ "description": "Creates an event based on a simple text string.", |
|
2269 |
+ "parameters": { |
|
2270 |
+ "calendarId": { |
|
2271 |
+ "type": "string", |
|
2272 |
+ "description": "Calendar identifier.", |
|
2273 |
+ "required": true, |
|
2274 |
+ "location": "path" |
|
2275 |
+ }, |
|
2276 |
+ "sendNotifications": { |
|
2277 |
+ "type": "boolean", |
|
2278 |
+ "description": "Whether to send notifications about the creation of the event. Optional. The default is False.", |
|
2279 |
+ "location": "query" |
|
2280 |
+ }, |
|
2281 |
+ "text": { |
|
2282 |
+ "type": "string", |
|
2283 |
+ "description": "The text describing the event to be created.", |
|
2284 |
+ "required": true, |
|
2285 |
+ "location": "query" |
|
2286 |
+ } |
|
2287 |
+ }, |
|
2288 |
+ "parameterOrder": [ |
|
2289 |
+ "calendarId", |
|
2290 |
+ "text" |
|
2291 |
+ ], |
|
2292 |
+ "response": { |
|
2293 |
+ "$ref": "Event" |
|
2294 |
+ }, |
|
2295 |
+ "scopes": [ |
|
2296 |
+ "https://www.googleapis.com/auth/calendar" |
|
2297 |
+ ] |
|
2298 |
+ }, |
|
2299 |
+ "update": { |
|
2300 |
+ "id": "calendar.events.update", |
|
2301 |
+ "path": "calendars/{calendarId}/events/{eventId}", |
|
2302 |
+ "httpMethod": "PUT", |
|
2303 |
+ "description": "Updates an event.", |
|
2304 |
+ "parameters": { |
|
2305 |
+ "alwaysIncludeEmail": { |
|
2306 |
+ "type": "boolean", |
|
2307 |
+ "description": "Whether to always include a value in the email field for the organizer, creator and attendees, even if no real email is available (i.e. a generated, non-working value will be provided). The use of this option is discouraged and should only be used by clients which cannot handle the absence of an email address value in the mentioned places. Optional. The default is False.", |
|
2308 |
+ "location": "query" |
|
2309 |
+ }, |
|
2310 |
+ "calendarId": { |
|
2311 |
+ "type": "string", |
|
2312 |
+ "description": "Calendar identifier.", |
|
2313 |
+ "required": true, |
|
2314 |
+ "location": "path" |
|
2315 |
+ }, |
|
2316 |
+ "eventId": { |
|
2317 |
+ "type": "string", |
|
2318 |
+ "description": "Event identifier.", |
|
2319 |
+ "required": true, |
|
2320 |
+ "location": "path" |
|
2321 |
+ }, |
|
2322 |
+ "maxAttendees": { |
|
2323 |
+ "type": "integer", |
|
2324 |
+ "description": "The maximum number of attendees to include in the response. If there are more than the specified number of attendees, only the participant is returned. Optional.", |
|
2325 |
+ "format": "int32", |
|
2326 |
+ "minimum": "1", |
|
2327 |
+ "location": "query" |
|
2328 |
+ }, |
|
2329 |
+ "sendNotifications": { |
|
2330 |
+ "type": "boolean", |
|
2331 |
+ "description": "Whether to send notifications about the event update (e.g. attendee's responses, title changes, etc.). Optional. The default is False.", |
|
2332 |
+ "location": "query" |
|
2333 |
+ } |
|
2334 |
+ }, |
|
2335 |
+ "parameterOrder": [ |
|
2336 |
+ "calendarId", |
|
2337 |
+ "eventId" |
|
2338 |
+ ], |
|
2339 |
+ "request": { |
|
2340 |
+ "$ref": "Event" |
|
2341 |
+ }, |
|
2342 |
+ "response": { |
|
2343 |
+ "$ref": "Event" |
|
2344 |
+ }, |
|
2345 |
+ "scopes": [ |
|
2346 |
+ "https://www.googleapis.com/auth/calendar" |
|
2347 |
+ ] |
|
2348 |
+ }, |
|
2349 |
+ "watch": { |
|
2350 |
+ "id": "calendar.events.watch", |
|
2351 |
+ "path": "calendars/{calendarId}/events/watch", |
|
2352 |
+ "httpMethod": "POST", |
|
2353 |
+ "description": "Watch for changes to Events resources.", |
|
2354 |
+ "parameters": { |
|
2355 |
+ "alwaysIncludeEmail": { |
|
2356 |
+ "type": "boolean", |
|
2357 |
+ "description": "Whether to always include a value in the email field for the organizer, creator and attendees, even if no real email is available (i.e. a generated, non-working value will be provided). The use of this option is discouraged and should only be used by clients which cannot handle the absence of an email address value in the mentioned places. Optional. The default is False.", |
|
2358 |
+ "location": "query" |
|
2359 |
+ }, |
|
2360 |
+ "calendarId": { |
|
2361 |
+ "type": "string", |
|
2362 |
+ "description": "Calendar identifier.", |
|
2363 |
+ "required": true, |
|
2364 |
+ "location": "path" |
|
2365 |
+ }, |
|
2366 |
+ "iCalUID": { |
|
2367 |
+ "type": "string", |
|
2368 |
+ "description": "Specifies event ID in the iCalendar format to be included in the response. Optional.", |
|
2369 |
+ "location": "query" |
|
2370 |
+ }, |
|
2371 |
+ "maxAttendees": { |
|
2372 |
+ "type": "integer", |
|
2373 |
+ "description": "The maximum number of attendees to include in the response. If there are more than the specified number of attendees, only the participant is returned. Optional.", |
|
2374 |
+ "format": "int32", |
|
2375 |
+ "minimum": "1", |
|
2376 |
+ "location": "query" |
|
2377 |
+ }, |
|
2378 |
+ "maxResults": { |
|
2379 |
+ "type": "integer", |
|
2380 |
+ "description": "Maximum number of events returned on one result page. By default the value is 250 events. The page size can never be larger than 2500 events. Optional.", |
|
2381 |
+ "format": "int32", |
|
2382 |
+ "minimum": "1", |
|
2383 |
+ "location": "query" |
|
2384 |
+ }, |
|
2385 |
+ "orderBy": { |
|
2386 |
+ "type": "string", |
|
2387 |
+ "description": "The order of the events returned in the result. Optional. The default is an unspecified, stable order.", |
|
2388 |
+ "enum": [ |
|
2389 |
+ "startTime", |
|
2390 |
+ "updated" |
|
2391 |
+ ], |
|
2392 |
+ "enumDescriptions": [ |
|
2393 |
+ "Order by the start date/time (ascending). This is only available when querying single events (i.e. the parameter singleEvents is True)", |
|
2394 |
+ "Order by last modification time (ascending)." |
|
2395 |
+ ], |
|
2396 |
+ "location": "query" |
|
2397 |
+ }, |
|
2398 |
+ "pageToken": { |
|
2399 |
+ "type": "string", |
|
2400 |
+ "description": "Token specifying which result page to return. Optional.", |
|
2401 |
+ "location": "query" |
|
2402 |
+ }, |
|
2403 |
+ "privateExtendedProperty": { |
|
2404 |
+ "type": "string", |
|
2405 |
+ "description": "Extended properties constraint specified as propertyName=value. Matches only private properties. This parameter might be repeated multiple times to return events that match all given constraints.", |
|
2406 |
+ "repeated": true, |
|
2407 |
+ "location": "query" |
|
2408 |
+ }, |
|
2409 |
+ "q": { |
|
2410 |
+ "type": "string", |
|
2411 |
+ "description": "Free text search terms to find events that match these terms in any field, except for extended properties. Optional.", |
|
2412 |
+ "location": "query" |
|
2413 |
+ }, |
|
2414 |
+ "sharedExtendedProperty": { |
|
2415 |
+ "type": "string", |
|
2416 |
+ "description": "Extended properties constraint specified as propertyName=value. Matches only shared properties. This parameter might be repeated multiple times to return events that match all given constraints.", |
|
2417 |
+ "repeated": true, |
|
2418 |
+ "location": "query" |
|
2419 |
+ }, |
|
2420 |
+ "showDeleted": { |
|
2421 |
+ "type": "boolean", |
|
2422 |
+ "description": "Whether to include deleted events (with status equals \"cancelled\") in the result. Cancelled instances of recurring events (but not the underlying recurring event) will still be included if showDeleted and singleEvents are both False. If showDeleted and singleEvents are both True, only single instances of deleted events (but not the underlying recurring events) are returned. Optional. The default is False.", |
|
2423 |
+ "location": "query" |
|
2424 |
+ }, |
|
2425 |
+ "showHiddenInvitations": { |
|
2426 |
+ "type": "boolean", |
|
2427 |
+ "description": "Whether to include hidden invitations in the result. Optional. The default is False.", |
|
2428 |
+ "location": "query" |
|
2429 |
+ }, |
|
2430 |
+ "singleEvents": { |
|
2431 |
+ "type": "boolean", |
|
2432 |
+ "description": "Whether to expand recurring events into instances and only return single one-off events and instances of recurring events, but not the underlying recurring events themselves. Optional. The default is False.", |
|
2433 |
+ "location": "query" |
|
2434 |
+ }, |
|
2435 |
+ "syncToken": { |
|
2436 |
+ "type": "string", |
|
2437 |
+ "description": "Token obtained from the nextSyncToken field returned on the last page of results from the previous list request. It makes the result of this list request contain only entries that have changed since then. All events deleted since the previous list request will always be in the result set and it is not allowed to set showDeleted to False.\nThere are several query parameters that cannot be specified together with nextSyncToken to ensure consistency of the client state.\n\nThese are: \n- iCalUID \n- orderBy \n- privateExtendedProperty \n- q \n- sharedExtendedProperty \n- timeMin \n- timeMax \n- updatedMin If the syncToken expires, the server will respond with a 410 GONE response code and the client should clear its storage and perform a full synchronization without any syncToken.\nLearn more about incremental synchronization.\nOptional. The default is to return all entries.", |
|
2438 |
+ "location": "query" |
|
2439 |
+ }, |
|
2440 |
+ "timeMax": { |
|
2441 |
+ "type": "string", |
|
2442 |
+ "description": "Upper bound (exclusive) for an event's start time to filter by. Optional. The default is not to filter by start time.", |
|
2443 |
+ "format": "date-time", |
|
2444 |
+ "location": "query" |
|
2445 |
+ }, |
|
2446 |
+ "timeMin": { |
|
2447 |
+ "type": "string", |
|
2448 |
+ "description": "Lower bound (inclusive) for an event's end time to filter by. Optional. The default is not to filter by end time.", |
|
2449 |
+ "format": "date-time", |
|
2450 |
+ "location": "query" |
|
2451 |
+ }, |
|
2452 |
+ "timeZone": { |
|
2453 |
+ "type": "string", |
|
2454 |
+ "description": "Time zone used in the response. Optional. The default is the time zone of the calendar.", |
|
2455 |
+ "location": "query" |
|
2456 |
+ }, |
|
2457 |
+ "updatedMin": { |
|
2458 |
+ "type": "string", |
|
2459 |
+ "description": "Lower bound for an event's last modification time (as a RFC 3339 timestamp) to filter by. When specified, entries deleted since this time will always be included regardless of showDeleted. Optional. The default is not to filter by last modification time.", |
|
2460 |
+ "format": "date-time", |
|
2461 |
+ "location": "query" |
|
2462 |
+ } |
|
2463 |
+ }, |
|
2464 |
+ "parameterOrder": [ |
|
2465 |
+ "calendarId" |
|
2466 |
+ ], |
|
2467 |
+ "request": { |
|
2468 |
+ "$ref": "Channel", |
|
2469 |
+ "parameterName": "resource" |
|
2470 |
+ }, |
|
2471 |
+ "response": { |
|
2472 |
+ "$ref": "Channel" |
|
2473 |
+ }, |
|
2474 |
+ "scopes": [ |
|
2475 |
+ "https://www.googleapis.com/auth/calendar", |
|
2476 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
2477 |
+ ], |
|
2478 |
+ "supportsSubscription": true |
|
2479 |
+ } |
|
2480 |
+ } |
|
2481 |
+ }, |
|
2482 |
+ "freebusy": { |
|
2483 |
+ "methods": { |
|
2484 |
+ "query": { |
|
2485 |
+ "id": "calendar.freebusy.query", |
|
2486 |
+ "path": "freeBusy", |
|
2487 |
+ "httpMethod": "POST", |
|
2488 |
+ "description": "Returns free/busy information for a set of calendars.", |
|
2489 |
+ "request": { |
|
2490 |
+ "$ref": "FreeBusyRequest" |
|
2491 |
+ }, |
|
2492 |
+ "response": { |
|
2493 |
+ "$ref": "FreeBusyResponse" |
|
2494 |
+ }, |
|
2495 |
+ "scopes": [ |
|
2496 |
+ "https://www.googleapis.com/auth/calendar", |
|
2497 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
2498 |
+ ] |
|
2499 |
+ } |
|
2500 |
+ } |
|
2501 |
+ }, |
|
2502 |
+ "settings": { |
|
2503 |
+ "methods": { |
|
2504 |
+ "get": { |
|
2505 |
+ "id": "calendar.settings.get", |
|
2506 |
+ "path": "users/me/settings/{setting}", |
|
2507 |
+ "httpMethod": "GET", |
|
2508 |
+ "description": "Returns a single user setting.", |
|
2509 |
+ "parameters": { |
|
2510 |
+ "setting": { |
|
2511 |
+ "type": "string", |
|
2512 |
+ "description": "The id of the user setting.", |
|
2513 |
+ "required": true, |
|
2514 |
+ "location": "path" |
|
2515 |
+ } |
|
2516 |
+ }, |
|
2517 |
+ "parameterOrder": [ |
|
2518 |
+ "setting" |
|
2519 |
+ ], |
|
2520 |
+ "response": { |
|
2521 |
+ "$ref": "Setting" |
|
2522 |
+ }, |
|
2523 |
+ "scopes": [ |
|
2524 |
+ "https://www.googleapis.com/auth/calendar", |
|
2525 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
2526 |
+ ] |
|
2527 |
+ }, |
|
2528 |
+ "list": { |
|
2529 |
+ "id": "calendar.settings.list", |
|
2530 |
+ "path": "users/me/settings", |
|
2531 |
+ "httpMethod": "GET", |
|
2532 |
+ "description": "Returns all user settings for the authenticated user.", |
|
2533 |
+ "parameters": { |
|
2534 |
+ "maxResults": { |
|
2535 |
+ "type": "integer", |
|
2536 |
+ "description": "Maximum number of entries returned on one result page. By default the value is 100 entries. The page size can never be larger than 250 entries. Optional.", |
|
2537 |
+ "format": "int32", |
|
2538 |
+ "minimum": "1", |
|
2539 |
+ "location": "query" |
|
2540 |
+ }, |
|
2541 |
+ "pageToken": { |
|
2542 |
+ "type": "string", |
|
2543 |
+ "description": "Token specifying which result page to return. Optional.", |
|
2544 |
+ "location": "query" |
|
2545 |
+ }, |
|
2546 |
+ "syncToken": { |
|
2547 |
+ "type": "string", |
|
2548 |
+ "description": "Token obtained from the nextSyncToken field returned on the last page of results from the previous list request. It makes the result of this list request contain only entries that have changed since then.\nIf the syncToken expires, the server will respond with a 410 GONE response code and the client should clear its storage and perform a full synchronization without any syncToken.\nLearn more about incremental synchronization.\nOptional. The default is to return all entries.", |
|
2549 |
+ "location": "query" |
|
2550 |
+ } |
|
2551 |
+ }, |
|
2552 |
+ "response": { |
|
2553 |
+ "$ref": "Settings" |
|
2554 |
+ }, |
|
2555 |
+ "scopes": [ |
|
2556 |
+ "https://www.googleapis.com/auth/calendar", |
|
2557 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
2558 |
+ ], |
|
2559 |
+ "supportsSubscription": true |
|
2560 |
+ }, |
|
2561 |
+ "watch": { |
|
2562 |
+ "id": "calendar.settings.watch", |
|
2563 |
+ "path": "users/me/settings/watch", |
|
2564 |
+ "httpMethod": "POST", |
|
2565 |
+ "description": "Watch for changes to Settings resources.", |
|
2566 |
+ "parameters": { |
|
2567 |
+ "maxResults": { |
|
2568 |
+ "type": "integer", |
|
2569 |
+ "description": "Maximum number of entries returned on one result page. By default the value is 100 entries. The page size can never be larger than 250 entries. Optional.", |
|
2570 |
+ "format": "int32", |
|
2571 |
+ "minimum": "1", |
|
2572 |
+ "location": "query" |
|
2573 |
+ }, |
|
2574 |
+ "pageToken": { |
|
2575 |
+ "type": "string", |
|
2576 |
+ "description": "Token specifying which result page to return. Optional.", |
|
2577 |
+ "location": "query" |
|
2578 |
+ }, |
|
2579 |
+ "syncToken": { |
|
2580 |
+ "type": "string", |
|
2581 |
+ "description": "Token obtained from the nextSyncToken field returned on the last page of results from the previous list request. It makes the result of this list request contain only entries that have changed since then.\nIf the syncToken expires, the server will respond with a 410 GONE response code and the client should clear its storage and perform a full synchronization without any syncToken.\nLearn more about incremental synchronization.\nOptional. The default is to return all entries.", |
|
2582 |
+ "location": "query" |
|
2583 |
+ } |
|
2584 |
+ }, |
|
2585 |
+ "request": { |
|
2586 |
+ "$ref": "Channel", |
|
2587 |
+ "parameterName": "resource" |
|
2588 |
+ }, |
|
2589 |
+ "response": { |
|
2590 |
+ "$ref": "Channel" |
|
2591 |
+ }, |
|
2592 |
+ "scopes": [ |
|
2593 |
+ "https://www.googleapis.com/auth/calendar", |
|
2594 |
+ "https://www.googleapis.com/auth/calendar.readonly" |
|
2595 |
+ ], |
|
2596 |
+ "supportsSubscription": true |
|
2597 |
+ } |
|
2598 |
+ } |
|
2599 |
+ } |
|
2600 |
+ } |
|
2601 |
+} |
@@ -10,8 +10,8 @@ jane_website_agent: |
||
10 | 10 |
:expected_update_period_in_days => 2, |
11 | 11 |
:mode => :on_change, |
12 | 12 |
:extract => { |
13 |
- :title => {:css => "item title", :text => true}, |
|
14 |
- :url => {:css => "item link", :text => true} |
|
13 |
+ :title => {:css => "item title", :value => './/text()'}, |
|
14 |
+ :url => {:css => "item link", :value => './/text()'} |
|
15 | 15 |
} |
16 | 16 |
}.to_json.inspect %> |
17 | 17 |
|
@@ -27,8 +27,8 @@ bob_website_agent: |
||
27 | 27 |
:expected_update_period_in_days => 2, |
28 | 28 |
:mode => :on_change, |
29 | 29 |
:extract => { |
30 |
- :url => {:css => "#comic img", :attr => "src"}, |
|
31 |
- :title => {:css => "#comic img", :attr => "title"} |
|
30 |
+ :url => {:css => "#comic img", :value => "@src"}, |
|
31 |
+ :title => {:css => "#comic img", :value => "@title"} |
|
32 | 32 |
} |
33 | 33 |
}.to_json.inspect %> |
34 | 34 |
|
@@ -1,12 +1,6 @@ |
||
1 | 1 |
require 'spec_helper' |
2 | 2 |
|
3 | 3 |
describe DotHelper do |
4 |
- describe "#dot_id" do |
|
5 |
- it "properly escapes double quotaion and backslash" do |
|
6 |
- dot_id('hello\\"').should == '"hello\\\\\\""' |
|
7 |
- end |
|
8 |
- end |
|
9 |
- |
|
10 | 4 |
describe "with example Agents" do |
11 | 5 |
class Agents::DotFoo < Agent |
12 | 6 |
default_schedule "2pm" |
@@ -30,18 +24,77 @@ describe DotHelper do |
||
30 | 24 |
end |
31 | 25 |
|
32 | 26 |
describe "#agents_dot" do |
27 |
+ before do |
|
28 |
+ @agents = [ |
|
29 |
+ @foo = Agents::DotFoo.new(name: "foo").tap { |agent| |
|
30 |
+ agent.user = users(:bob) |
|
31 |
+ agent.save! |
|
32 |
+ }, |
|
33 |
+ |
|
34 |
+ @bar1 = Agents::DotBar.new(name: "bar1").tap { |agent| |
|
35 |
+ agent.user = users(:bob) |
|
36 |
+ agent.sources << @foo |
|
37 |
+ agent.save! |
|
38 |
+ }, |
|
39 |
+ |
|
40 |
+ @bar2 = Agents::DotBar.new(name: "bar2").tap { |agent| |
|
41 |
+ agent.user = users(:bob) |
|
42 |
+ agent.sources << @foo |
|
43 |
+ agent.propagate_immediately = true |
|
44 |
+ agent.disabled = true |
|
45 |
+ agent.save! |
|
46 |
+ }, |
|
47 |
+ |
|
48 |
+ @bar3 = Agents::DotBar.new(name: "bar3").tap { |agent| |
|
49 |
+ agent.user = users(:bob) |
|
50 |
+ agent.sources << @bar2 |
|
51 |
+ agent.save! |
|
52 |
+ }, |
|
53 |
+ ] |
|
54 |
+ end |
|
55 |
+ |
|
33 | 56 |
it "generates a DOT script" do |
34 |
- @foo = Agents::DotFoo.new(:name => "foo") |
|
35 |
- @foo.user = users(:bob) |
|
36 |
- @foo.save! |
|
57 |
+ agents_dot(@agents).should =~ %r{ |
|
58 |
+ \A |
|
59 |
+ digraph \s foo \{ |
|
60 |
+ node \[ [^\]]+ \]; |
|
61 |
+ (?<foo>\w+) \[label=foo\]; |
|
62 |
+ \k<foo> -> (?<bar1>\w+) \[style=dashed\]; |
|
63 |
+ \k<foo> -> (?<bar2>\w+) \[color="\#999999"\]; |
|
64 |
+ \k<bar1> \[label=bar1\]; |
|
65 |
+ \k<bar2> \[label="bar2 \s \(Disabled\)",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; |
|
66 |
+ \k<bar2> -> (?<bar3>\w+) \[style=dashed,color="\#999999"\]; |
|
67 |
+ \k<bar3> \[label=bar3\]; |
|
68 |
+ \} |
|
69 |
+ \z |
|
70 |
+ }x |
|
71 |
+ end |
|
37 | 72 |
|
38 |
- @bar = Agents::DotBar.new(:name => "bar") |
|
39 |
- @bar.user = users(:bob) |
|
40 |
- @bar.sources << @foo |
|
41 |
- @bar.save! |
|
73 |
+ it "generates a richer DOT script" do |
|
74 |
+ agents_dot(@agents, true).should =~ %r{ |
|
75 |
+ \A |
|
76 |
+ digraph \s foo \{ |
|
77 |
+ node \[ [^\]]+ \]; |
|
78 |
+ (?<foo>\w+) \[label=foo,URL="#{Regexp.quote(agent_path(@foo))}"\]; |
|
79 |
+ \k<foo> -> (?<bar1>\w+) \[style=dashed\]; |
|
80 |
+ \k<foo> -> (?<bar2>\w+) \[color="\#999999"\]; |
|
81 |
+ \k<bar1> \[label=bar1,URL="#{Regexp.quote(agent_path(@bar1))}"\]; |
|
82 |
+ \k<bar2> \[label="bar2 \s \(Disabled\)",URL="#{Regexp.quote(agent_path(@bar2))}",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; |
|
83 |
+ \k<bar2> -> (?<bar3>\w+) \[style=dashed,color="\#999999"\]; |
|
84 |
+ \k<bar3> \[label=bar3,URL="#{Regexp.quote(agent_path(@bar3))}"\]; |
|
85 |
+ \} |
|
86 |
+ \z |
|
87 |
+ }x |
|
88 |
+ end |
|
89 |
+ end |
|
90 |
+ end |
|
42 | 91 |
|
43 |
- agents_dot([@foo, @bar]).should == 'digraph foo {"foo";"foo"->"bar";"bar";}' |
|
44 |
- agents_dot([@foo, @bar], true).should == 'digraph foo {"foo"[URL="/agents/%d"];"foo"->"bar";"bar"[URL="/agents/%d"];}' % [@foo.id, @bar.id] |
|
92 |
+ describe DotHelper::DotDrawer do |
|
93 |
+ describe "#id" do |
|
94 |
+ it "properly escapes double quotaion and backslash" do |
|
95 |
+ DotHelper::DotDrawer.draw(foo: "") { |
|
96 |
+ id('hello\\"') |
|
97 |
+ }.should == '"hello\\\\\\""' |
|
45 | 98 |
end |
46 | 99 |
end |
47 | 100 |
end |
@@ -22,8 +22,8 @@ describe Utils do |
||
22 | 22 |
|
23 | 23 |
Utils.unindent("Hello\n I am indented").should == "Hello\n I am indented" |
24 | 24 |
|
25 |
- a = " Events will have the fields you specified. Your options look like:\n\n {\n \"url\": {\n \"css\": \"#comic img\",\n \"attr\": \"src\"\n },\n \"title\": {\n \"css\": \"#comic img\",\n \"attr\": \"title\"\n }\n }\"\n" |
|
26 |
- Utils.unindent(a).should == "Events will have the fields you specified. Your options look like:\n\n {\n \"url\": {\n\"css\": \"#comic img\",\n\"attr\": \"src\"\n },\n \"title\": {\n\"css\": \"#comic img\",\n\"attr\": \"title\"\n }\n }\"" |
|
25 |
+ a = " Events will have the fields you specified. Your options look like:\n\n {\n \"url\": {\n \"css\": \"#comic img\",\n \"value\": \"@src\"\n },\n \"title\": {\n \"css\": \"#comic img\",\n \"value\": \"@title\"\n }\n }\"\n" |
|
26 |
+ Utils.unindent(a).should == "Events will have the fields you specified. Your options look like:\n\n {\n \"url\": {\n\"css\": \"#comic img\",\n\"value\": \"@src\"\n },\n \"title\": {\n\"css\": \"#comic img\",\n\"value\": \"@title\"\n }\n }\"" |
|
27 | 27 |
end |
28 | 28 |
end |
29 | 29 |
|
@@ -114,4 +114,4 @@ describe Utils do |
||
114 | 114 |
cleaned_json.should include("<\\/script>") |
115 | 115 |
end |
116 | 116 |
end |
117 |
-end |
|
117 |
+end |
@@ -132,6 +132,13 @@ describe Agent do |
||
132 | 132 |
it_behaves_like HasGuid |
133 | 133 |
end |
134 | 134 |
|
135 |
+ describe ".short_type" do |
|
136 |
+ it "returns a short name without 'Agents::'" do |
|
137 |
+ Agents::SomethingSource.new.short_type.should == "SomethingSource" |
|
138 |
+ Agents::CannotBeScheduled.new.short_type.should == "CannotBeScheduled" |
|
139 |
+ end |
|
140 |
+ end |
|
141 |
+ |
|
135 | 142 |
describe ".default_schedule" do |
136 | 143 |
it "stores the default on the class" do |
137 | 144 |
Agents::SomethingSource.default_schedule.should == "2pm" |
@@ -729,3 +736,98 @@ describe Agent do |
||
729 | 736 |
end |
730 | 737 |
end |
731 | 738 |
end |
739 |
+ |
|
740 |
+describe AgentDrop do |
|
741 |
+ def interpolate(string, agent) |
|
742 |
+ agent.interpolate_string(string, "agent" => agent) |
|
743 |
+ end |
|
744 |
+ |
|
745 |
+ before do |
|
746 |
+ @wsa1 = Agents::WebsiteAgent.new( |
|
747 |
+ name: 'XKCD', |
|
748 |
+ options: { |
|
749 |
+ expected_update_period_in_days: 2, |
|
750 |
+ type: 'html', |
|
751 |
+ url: 'http://xkcd.com/', |
|
752 |
+ mode: 'on_change', |
|
753 |
+ extract: { |
|
754 |
+ url: { css: '#comic img', value: '@src' }, |
|
755 |
+ title: { css: '#comic img', value: '@alt' }, |
|
756 |
+ }, |
|
757 |
+ }, |
|
758 |
+ schedule: 'every_1h', |
|
759 |
+ keep_events_for: 2) |
|
760 |
+ @wsa1.user = users(:bob) |
|
761 |
+ @wsa1.save! |
|
762 |
+ |
|
763 |
+ @wsa2 = Agents::WebsiteAgent.new( |
|
764 |
+ name: 'Dilbert', |
|
765 |
+ options: { |
|
766 |
+ expected_update_period_in_days: 2, |
|
767 |
+ type: 'html', |
|
768 |
+ url: 'http://dilbert.com/', |
|
769 |
+ mode: 'on_change', |
|
770 |
+ extract: { |
|
771 |
+ url: { css: '[id^=strip_enlarged_] img', value: '@src' }, |
|
772 |
+ title: { css: '.STR_DateStrip', value: './/text()' }, |
|
773 |
+ }, |
|
774 |
+ }, |
|
775 |
+ schedule: 'every_12h', |
|
776 |
+ keep_events_for: 2) |
|
777 |
+ @wsa2.user = users(:bob) |
|
778 |
+ @wsa2.save! |
|
779 |
+ |
|
780 |
+ @efa = Agents::EventFormattingAgent.new( |
|
781 |
+ name: 'Formatter', |
|
782 |
+ options: { |
|
783 |
+ instructions: { |
|
784 |
+ message: '{{agent.name}}: {{title}} {{url}}', |
|
785 |
+ agent: '{{agent.type}}', |
|
786 |
+ }, |
|
787 |
+ mode: 'clean', |
|
788 |
+ matchers: [], |
|
789 |
+ skip_created_at: 'false', |
|
790 |
+ }, |
|
791 |
+ keep_events_for: 2, |
|
792 |
+ propagate_immediately: true) |
|
793 |
+ @efa.user = users(:bob) |
|
794 |
+ @efa.sources << @wsa1 << @wsa2 |
|
795 |
+ @efa.memory[:test] = 1 |
|
796 |
+ @efa.save! |
|
797 |
+ end |
|
798 |
+ |
|
799 |
+ it 'should be created via Agent#to_liquid' do |
|
800 |
+ @wsa1.to_liquid.class.should be(AgentDrop) |
|
801 |
+ @wsa2.to_liquid.class.should be(AgentDrop) |
|
802 |
+ @efa.to_liquid.class.should be(AgentDrop) |
|
803 |
+ end |
|
804 |
+ |
|
805 |
+ it 'should have .type and .name' do |
|
806 |
+ t = '{{agent.type}}: {{agent.name}}' |
|
807 |
+ interpolate(t, @wsa1).should eq('WebsiteAgent: XKCD') |
|
808 |
+ interpolate(t, @wsa2).should eq('WebsiteAgent: Dilbert') |
|
809 |
+ interpolate(t, @efa).should eq('EventFormattingAgent: Formatter') |
|
810 |
+ end |
|
811 |
+ |
|
812 |
+ it 'should have .options' do |
|
813 |
+ t = '{{agent.options.url}}' |
|
814 |
+ interpolate(t, @wsa1).should eq('http://xkcd.com/') |
|
815 |
+ interpolate(t, @wsa2).should eq('http://dilbert.com/') |
|
816 |
+ interpolate('{{agent.options.instructions.message}}', |
|
817 |
+ @efa).should eq('{{agent.name}}: {{title}} {{url}}') |
|
818 |
+ end |
|
819 |
+ |
|
820 |
+ it 'should have .sources' do |
|
821 |
+ t = '{{agent.sources.size}}: {{agent.sources | map:"name" | join:", "}}' |
|
822 |
+ interpolate(t, @wsa1).should eq('0: ') |
|
823 |
+ interpolate(t, @wsa2).should eq('0: ') |
|
824 |
+ interpolate(t, @efa).should eq('2: XKCD, Dilbert') |
|
825 |
+ end |
|
826 |
+ |
|
827 |
+ it 'should have .receivers' do |
|
828 |
+ t = '{{agent.receivers.size}}: {{agent.receivers | map:"name" | join:", "}}' |
|
829 |
+ interpolate(t, @wsa1).should eq('1: Formatter') |
|
830 |
+ interpolate(t, @wsa2).should eq('1: Formatter') |
|
831 |
+ interpolate(t, @efa).should eq('0: ') |
|
832 |
+ end |
|
833 |
+end |
@@ -1,12 +1,14 @@ |
||
1 | 1 |
require 'spec_helper' |
2 | 2 |
|
3 | 3 |
describe Agents::EmailAgent do |
4 |
+ it_behaves_like EmailConcern |
|
5 |
+ |
|
4 | 6 |
def get_message_part(mail, content_type) |
5 | 7 |
mail.body.parts.find { |p| p.content_type.match content_type }.body.raw_source |
6 | 8 |
end |
7 | 9 |
|
8 | 10 |
before do |
9 |
- @checker = Agents::EmailAgent.new(:name => "something", :options => { :expected_receive_period_in_days => 2, :subject => "something interesting" }) |
|
11 |
+ @checker = Agents::EmailAgent.new(:name => "something", :options => { :expected_receive_period_in_days => "2", :subject => "something interesting" }) |
|
10 | 12 |
@checker.user = users(:bob) |
11 | 13 |
@checker.save! |
12 | 14 |
end |
@@ -54,6 +56,5 @@ describe Agents::EmailAgent do |
||
54 | 56 |
plain_email_text.should =~ /avehumidity/ |
55 | 57 |
html_email_text.should =~ /avehumidity/ |
56 | 58 |
end |
57 |
- |
|
58 | 59 |
end |
59 | 60 |
end |
@@ -1,12 +1,14 @@ |
||
1 | 1 |
require 'spec_helper' |
2 | 2 |
|
3 | 3 |
describe Agents::EmailDigestAgent do |
4 |
+ it_behaves_like EmailConcern |
|
5 |
+ |
|
4 | 6 |
def get_message_part(mail, content_type) |
5 | 7 |
mail.body.parts.find { |p| p.content_type.match content_type }.body.raw_source |
6 | 8 |
end |
7 | 9 |
|
8 | 10 |
before do |
9 |
- @checker = Agents::EmailDigestAgent.new(:name => "something", :options => { :expected_receive_period_in_days => 2, :subject => "something interesting" }) |
|
11 |
+ @checker = Agents::EmailDigestAgent.new(:name => "something", :options => { :expected_receive_period_in_days => "2", :subject => "something interesting" }) |
|
10 | 12 |
@checker.user = users(:bob) |
11 | 13 |
@checker.save! |
12 | 14 |
end |
@@ -7,7 +7,8 @@ describe Agents::EventFormattingAgent do |
||
7 | 7 |
:options => { |
8 | 8 |
:instructions => { |
9 | 9 |
:message => "Received {{content.text}} from {{content.name}} .", |
10 |
- :subject => "Weather looks like {{conditions}} according to the forecast at {{pretty_date.time}}" |
|
10 |
+ :subject => "Weather looks like {{conditions}} according to the forecast at {{pretty_date.time}}", |
|
11 |
+ :agent => "{{agent.type}}", |
|
11 | 12 |
}, |
12 | 13 |
:mode => "clean", |
13 | 14 |
:matchers => [ |
@@ -17,7 +18,6 @@ describe Agents::EventFormattingAgent do |
||
17 | 18 |
:to => "pretty_date", |
18 | 19 |
}, |
19 | 20 |
], |
20 |
- :skip_agent => "false", |
|
21 | 21 |
:skip_created_at => "false" |
22 | 22 |
} |
23 | 23 |
} |
@@ -53,14 +53,6 @@ describe Agents::EventFormattingAgent do |
||
53 | 53 |
Event.last.payload[:content].should_not == nil |
54 | 54 |
end |
55 | 55 |
|
56 |
- it "should accept skip_agent" do |
|
57 |
- @checker.receive([@event]) |
|
58 |
- Event.last.payload[:agent].should == "WeatherAgent" |
|
59 |
- @checker.options[:skip_agent] = "true" |
|
60 |
- @checker.receive([@event]) |
|
61 |
- Event.last.payload[:agent].should == nil |
|
62 |
- end |
|
63 |
- |
|
64 | 56 |
it "should accept skip_created_at" do |
65 | 57 |
@checker.receive([@event]) |
66 | 58 |
Event.last.payload[:created_at].should_not == nil |
@@ -69,12 +61,13 @@ describe Agents::EventFormattingAgent do |
||
69 | 61 |
Event.last.payload[:created_at].should == nil |
70 | 62 |
end |
71 | 63 |
|
72 |
- it "should handle JSONPaths in instructions" do |
|
64 |
+ it "should handle Liquid templating in instructions" do |
|
73 | 65 |
@checker.receive([@event]) |
74 | 66 |
Event.last.payload[:message].should == "Received Some Lorem Ipsum from somevalue ." |
67 |
+ Event.last.payload[:agent].should == "WeatherAgent" |
|
75 | 68 |
end |
76 | 69 |
|
77 |
- it "should handle matchers and JSONPaths in instructions" do |
|
70 |
+ it "should handle matchers and Liquid templating in instructions" do |
|
78 | 71 |
@checker.receive([@event]) |
79 | 72 |
Event.last.payload[:subject].should == "Weather looks like someothervalue according to the forecast at 10:00 PM EST" |
80 | 73 |
end |
@@ -152,11 +145,6 @@ describe Agents::EventFormattingAgent do |
||
152 | 145 |
@checker.should_not be_valid |
153 | 146 |
end |
154 | 147 |
|
155 |
- it "should validate presence of skip_agent" do |
|
156 |
- @checker.options[:skip_agent] = "" |
|
157 |
- @checker.should_not be_valid |
|
158 |
- end |
|
159 |
- |
|
160 | 148 |
it "should validate presence of skip_created_at" do |
161 | 149 |
@checker.options[:skip_created_at] = "" |
162 | 150 |
@checker.should_not be_valid |
@@ -0,0 +1,43 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Agents::GoogleCalendarPublishAgent, :vcr do |
|
4 |
+ before do |
|
5 |
+ @valid_params = { |
|
6 |
+ 'expected_update_period_in_days' => "10", |
|
7 |
+ 'calendar_id' => 'sqv39gj35tc837gdns1g4d81cg@group.calendar.google.com', |
|
8 |
+ 'google' => { |
|
9 |
+ 'key_file' => File.dirname(__FILE__) + '/../../data_fixtures/private.key', |
|
10 |
+ 'key_secret' => 'notasecret', |
|
11 |
+ 'service_account_email' => '1029936966326-ncjd7776pcspc98hsg82gsb56t3217ef@developer.gserviceaccount.com' |
|
12 |
+ } |
|
13 |
+ } |
|
14 |
+ @checker = Agents::GoogleCalendarPublishAgent.new(:name => "somename", :options => @valid_params) |
|
15 |
+ @checker.user = users(:jane) |
|
16 |
+ @checker.save! |
|
17 |
+ end |
|
18 |
+ |
|
19 |
+ describe '#receive' do |
|
20 |
+ it 'should publish any payload it receives' do |
|
21 |
+ event1 = Event.new |
|
22 |
+ event1.agent = agents(:bob_manual_event_agent) |
|
23 |
+ event1.payload = { |
|
24 |
+ 'message' => { |
|
25 |
+ 'visibility' => 'default', |
|
26 |
+ 'summary' => "Awesome event", |
|
27 |
+ 'description' => "An example event with text. Pro tip: DateTimes are in RFC3339", |
|
28 |
+ 'end' => { |
|
29 |
+ 'dateTime' => '2014-10-02T11:00:00-05:00' |
|
30 |
+ }, |
|
31 |
+ 'start' => { |
|
32 |
+ 'dateTime' => '2014-10-02T10:00:00-05:00' |
|
33 |
+ } |
|
34 |
+ } |
|
35 |
+ } |
|
36 |
+ event1.save! |
|
37 |
+ |
|
38 |
+ @checker.receive([event1]) |
|
39 |
+ |
|
40 |
+ @checker.events.count.should eq(1) |
|
41 |
+ end |
|
42 |
+ end |
|
43 |
+end |
@@ -24,7 +24,7 @@ describe Agents::ImapFolderAgent do |
||
24 | 24 |
end |
25 | 25 |
|
26 | 26 |
def uidvalidity |
27 |
- '100' |
|
27 |
+ 100 |
|
28 | 28 |
end |
29 | 29 |
|
30 | 30 |
def has_attachment? |
@@ -53,7 +53,15 @@ describe Agents::ImapFolderAgent do |
||
53 | 53 |
] |
54 | 54 |
|
55 | 55 |
stub(@checker).each_unread_mail.returns { |yielder| |
56 |
- @mails.each(&yielder) |
|
56 |
+ seen = @checker.lastseen |
|
57 |
+ notified = @checker.notified |
|
58 |
+ @mails.each_with_object(notified) { |mail| |
|
59 |
+ yielder[mail, notified] |
|
60 |
+ seen[mail.uidvalidity] = mail.uid |
|
61 |
+ } |
|
62 |
+ @checker.lastseen = seen |
|
63 |
+ @checker.notified = notified |
|
64 |
+ nil |
|
57 | 65 |
} |
58 | 66 |
|
59 | 67 |
@payloads = [ |
@@ -110,11 +118,19 @@ describe Agents::ImapFolderAgent do |
||
110 | 118 |
end |
111 | 119 |
|
112 | 120 |
it 'should validate the boolean fields' do |
113 |
- @checker.options['ssl'] = false |
|
114 |
- @checker.should be_valid |
|
121 |
+ %w[ssl mark_as_read].each do |key| |
|
122 |
+ @checker.options[key] = 1 |
|
123 |
+ @checker.should_not be_valid |
|
115 | 124 |
|
116 |
- @checker.options['ssl'] = 'true' |
|
117 |
- @checker.should_not be_valid |
|
125 |
+ @checker.options[key] = false |
|
126 |
+ @checker.should be_valid |
|
127 |
+ |
|
128 |
+ @checker.options[key] = 'true' |
|
129 |
+ @checker.should be_valid |
|
130 |
+ |
|
131 |
+ @checker.options[key] = '' |
|
132 |
+ @checker.should be_valid |
|
133 |
+ end |
|
118 | 134 |
end |
119 | 135 |
|
120 | 136 |
it 'should validate regexp conditions' do |
@@ -139,9 +155,9 @@ describe Agents::ImapFolderAgent do |
||
139 | 155 |
describe '#check' do |
140 | 156 |
it 'should check for mails and save memory' do |
141 | 157 |
lambda { @checker.check }.should change { Event.count }.by(2) |
142 |
- @checker.memory['notified'].sort.should == @mails.map(&:message_id).sort |
|
143 |
- @checker.memory['seen'].should == @mails.each_with_object({}) { |mail, seen| |
|
144 |
- (seen[mail.uidvalidity] ||= []) << mail.uid |
|
158 |
+ @checker.notified.sort.should == @mails.map(&:message_id).sort |
|
159 |
+ @checker.lastseen.should == @mails.each_with_object(@checker.make_seen) { |mail, seen| |
|
160 |
+ seen[mail.uidvalidity] = mail.uid |
|
145 | 161 |
} |
146 | 162 |
|
147 | 163 |
Event.last(2).map(&:payload) == @payloads |
@@ -153,9 +169,9 @@ describe Agents::ImapFolderAgent do |
||
153 | 169 |
@checker.options['conditions']['to'] = 'John.Doe@*' |
154 | 170 |
|
155 | 171 |
lambda { @checker.check }.should change { Event.count }.by(1) |
156 |
- @checker.memory['notified'].sort.should == [@mails.first.message_id] |
|
157 |
- @checker.memory['seen'].should == @mails.each_with_object({}) { |mail, seen| |
|
158 |
- (seen[mail.uidvalidity] ||= []) << mail.uid |
|
172 |
+ @checker.notified.sort.should == [@mails.first.message_id] |
|
173 |
+ @checker.lastseen.should == @mails.each_with_object(@checker.make_seen) { |mail, seen| |
|
174 |
+ seen[mail.uidvalidity] = mail.uid |
|
159 | 175 |
} |
160 | 176 |
|
161 | 177 |
Event.last.payload.should == @payloads.first |
@@ -170,9 +186,9 @@ describe Agents::ImapFolderAgent do |
||
170 | 186 |
) |
171 | 187 |
|
172 | 188 |
lambda { @checker.check }.should change { Event.count }.by(1) |
173 |
- @checker.memory['notified'].sort.should == [@mails.last.message_id] |
|
174 |
- @checker.memory['seen'].should == @mails.each_with_object({}) { |mail, seen| |
|
175 |
- (seen[mail.uidvalidity] ||= []) << mail.uid |
|
189 |
+ @checker.notified.sort.should == [@mails.last.message_id] |
|
190 |
+ @checker.lastseen.should == @mails.each_with_object(@checker.make_seen) { |mail, seen| |
|
191 |
+ seen[mail.uidvalidity] = mail.uid |
|
176 | 192 |
} |
177 | 193 |
|
178 | 194 |
Event.last.payload.should == @payloads.last.update( |
@@ -208,9 +224,9 @@ describe Agents::ImapFolderAgent do |
||
208 | 224 |
) |
209 | 225 |
|
210 | 226 |
lambda { @checker.check }.should_not change { Event.count } |
211 |
- @checker.memory['notified'].sort.should == [] |
|
212 |
- @checker.memory['seen'].should == @mails.each_with_object({}) { |mail, seen| |
|
213 |
- (seen[mail.uidvalidity] ||= []) << mail.uid |
|
227 |
+ @checker.notified.sort.should == [] |
|
228 |
+ @checker.lastseen.should == @mails.each_with_object(@checker.make_seen) { |mail, seen| |
|
229 |
+ seen[mail.uidvalidity] = mail.uid |
|
214 | 230 |
} |
215 | 231 |
end |
216 | 232 |
|
@@ -25,11 +25,25 @@ describe Agents::PostAgent do |
||
25 | 25 |
'somekey' => 'value' |
26 | 26 |
} |
27 | 27 |
} |
28 |
+ @requests = 0 |
|
29 |
+ @sent_requests = { Net::HTTP::Get => [], Net::HTTP::Post => [], Net::HTTP::Put => [], Net::HTTP::Delete => [], Net::HTTP::Patch => [] } |
|
28 | 30 |
|
29 |
- @sent_posts = [] |
|
30 |
- @sent_gets = [] |
|
31 |
- stub.any_instance_of(Agents::PostAgent).post_data { |data| @sent_posts << data } |
|
32 |
- stub.any_instance_of(Agents::PostAgent).get_data { |data| @sent_gets << data } |
|
31 |
+ stub.any_instance_of(Agents::PostAgent).post_data { |data, payload, type| @requests += 1; @sent_requests[type] << data } |
|
32 |
+ stub.any_instance_of(Agents::PostAgent).get_data { |data, payload| @requests += 1; @sent_requests[Net::HTTP::Get] << data } |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ describe "making requests" do |
|
36 |
+ it "can make requests of each type" do |
|
37 |
+ { 'get' => Net::HTTP::Get, 'put' => Net::HTTP::Put, |
|
38 |
+ 'post' => Net::HTTP::Post, 'patch' => Net::HTTP::Patch, |
|
39 |
+ 'delete' => Net::HTTP::Delete }.each.with_index do |(verb, type), index| |
|
40 |
+ @checker.options['method'] = verb |
|
41 |
+ @checker.should be_valid |
|
42 |
+ @checker.check |
|
43 |
+ @requests.should == index + 1 |
|
44 |
+ @sent_requests[type].length.should == 1 |
|
45 |
+ end |
|
46 |
+ end |
|
33 | 47 |
end |
34 | 48 |
|
35 | 49 |
describe "#receive" do |
@@ -45,11 +59,11 @@ describe Agents::PostAgent do |
||
45 | 59 |
lambda { |
46 | 60 |
lambda { |
47 | 61 |
@checker.receive([@event, event1]) |
48 |
- }.should change { @sent_posts.length }.by(2) |
|
49 |
- }.should_not change { @sent_gets.length } |
|
62 |
+ }.should change { @sent_requests[Net::HTTP::Post].length }.by(2) |
|
63 |
+ }.should_not change { @sent_requests[Net::HTTP::Get].length } |
|
50 | 64 |
|
51 |
- @sent_posts[0].should == @event.payload.merge('default' => 'value') |
|
52 |
- @sent_posts[1].should == event1.payload |
|
65 |
+ @sent_requests[Net::HTTP::Post][0].should == @event.payload.merge('default' => 'value') |
|
66 |
+ @sent_requests[Net::HTTP::Post][1].should == event1.payload |
|
53 | 67 |
end |
54 | 68 |
|
55 | 69 |
it "can make GET requests" do |
@@ -58,10 +72,19 @@ describe Agents::PostAgent do |
||
58 | 72 |
lambda { |
59 | 73 |
lambda { |
60 | 74 |
@checker.receive([@event]) |
61 |
- }.should change { @sent_gets.length }.by(1) |
|
62 |
- }.should_not change { @sent_posts.length } |
|
75 |
+ }.should change { @sent_requests[Net::HTTP::Get].length }.by(1) |
|
76 |
+ }.should_not change { @sent_requests[Net::HTTP::Post].length } |
|
77 |
+ |
|
78 |
+ @sent_requests[Net::HTTP::Get][0].should == @event.payload.merge('default' => 'value') |
|
79 |
+ end |
|
63 | 80 |
|
64 |
- @sent_gets[0].should == @event.payload.merge('default' => 'value') |
|
81 |
+ it "can skip merging the incoming event when no_merge is set, but it still interpolates" do |
|
82 |
+ @checker.options['no_merge'] = 'true' |
|
83 |
+ @checker.options['payload'] = { |
|
84 |
+ 'key' => 'it said: {{ someotherkey.somekey }}' |
|
85 |
+ } |
|
86 |
+ @checker.receive([@event]) |
|
87 |
+ @sent_requests[Net::HTTP::Post].first.should == { 'key' => 'it said: value' } |
|
65 | 88 |
end |
66 | 89 |
end |
67 | 90 |
|
@@ -69,9 +92,9 @@ describe Agents::PostAgent do |
||
69 | 92 |
it "sends options['payload'] as a POST request" do |
70 | 93 |
lambda { |
71 | 94 |
@checker.check |
72 |
- }.should change { @sent_posts.length }.by(1) |
|
95 |
+ }.should change { @sent_requests[Net::HTTP::Post].length }.by(1) |
|
73 | 96 |
|
74 |
- @sent_posts[0].should == @checker.options['payload'] |
|
97 |
+ @sent_requests[Net::HTTP::Post][0].should == @checker.options['payload'] |
|
75 | 98 |
end |
76 | 99 |
|
77 | 100 |
it "sends options['payload'] as a GET request" do |
@@ -79,10 +102,10 @@ describe Agents::PostAgent do |
||
79 | 102 |
lambda { |
80 | 103 |
lambda { |
81 | 104 |
@checker.check |
82 |
- }.should change { @sent_gets.length }.by(1) |
|
83 |
- }.should_not change { @sent_posts.length } |
|
105 |
+ }.should change { @sent_requests[Net::HTTP::Get].length }.by(1) |
|
106 |
+ }.should_not change { @sent_requests[Net::HTTP::Post].length } |
|
84 | 107 |
|
85 |
- @sent_gets[0].should == @checker.options['payload'] |
|
108 |
+ @sent_requests[Net::HTTP::Get][0].should == @checker.options['payload'] |
|
86 | 109 |
end |
87 | 110 |
end |
88 | 111 |
|
@@ -112,7 +135,7 @@ describe Agents::PostAgent do |
||
112 | 135 |
@checker.should_not be_valid |
113 | 136 |
end |
114 | 137 |
|
115 |
- it "should validate method as post or get, defaulting to post" do |
|
138 |
+ it "should validate method as post, get, put, patch, or delete, defaulting to post" do |
|
116 | 139 |
@checker.options['method'] = "" |
117 | 140 |
@checker.method.should == "post" |
118 | 141 |
@checker.should be_valid |
@@ -125,11 +148,35 @@ describe Agents::PostAgent do |
||
125 | 148 |
@checker.method.should == "get" |
126 | 149 |
@checker.should be_valid |
127 | 150 |
|
151 |
+ @checker.options['method'] = "patch" |
|
152 |
+ @checker.method.should == "patch" |
|
153 |
+ @checker.should be_valid |
|
154 |
+ |
|
128 | 155 |
@checker.options['method'] = "wut" |
129 | 156 |
@checker.method.should == "wut" |
130 | 157 |
@checker.should_not be_valid |
131 | 158 |
end |
132 | 159 |
|
160 |
+ it "should validate that no_merge is 'true' or 'false', if present" do |
|
161 |
+ @checker.options['no_merge'] = "" |
|
162 |
+ @checker.should be_valid |
|
163 |
+ |
|
164 |
+ @checker.options['no_merge'] = "true" |
|
165 |
+ @checker.should be_valid |
|
166 |
+ |
|
167 |
+ @checker.options['no_merge'] = "false" |
|
168 |
+ @checker.should be_valid |
|
169 |
+ |
|
170 |
+ @checker.options['no_merge'] = false |
|
171 |
+ @checker.should be_valid |
|
172 |
+ |
|
173 |
+ @checker.options['no_merge'] = true |
|
174 |
+ @checker.should be_valid |
|
175 |
+ |
|
176 |
+ @checker.options['no_merge'] = 'blarg' |
|
177 |
+ @checker.should_not be_valid |
|
178 |
+ end |
|
179 |
+ |
|
133 | 180 |
it "should validate payload as a hash, if present" do |
134 | 181 |
@checker.options['payload'] = "" |
135 | 182 |
@checker.should be_valid |
@@ -178,7 +225,17 @@ describe Agents::PostAgent do |
||
178 | 225 |
it "just returns the post_uri when no params are given" do |
179 | 226 |
@checker.options['post_url'] = "http://example.com/a/path?existing_param=existing_value" |
180 | 227 |
uri = @checker.generate_uri |
228 |
+ uri.host.should == 'example.com' |
|
229 |
+ uri.scheme.should == 'http' |
|
181 | 230 |
uri.request_uri.should == "/a/path?existing_param=existing_value" |
182 | 231 |
end |
232 |
+ |
|
233 |
+ it "interpolates when receiving a payload" do |
|
234 |
+ @checker.options['post_url'] = "https://{{ domain }}/{{ variable }}?existing_param=existing_value" |
|
235 |
+ uri = @checker.generate_uri({ "some_param" => "some_value", "another_param" => "another_value" }, { 'domain' => 'google.com', 'variable' => 'a_variable' }) |
|
236 |
+ uri.request_uri.should == "/a_variable?existing_param=existing_value&some_param=some_value&another_param=another_value" |
|
237 |
+ uri.host.should == 'google.com' |
|
238 |
+ uri.scheme.should == 'https' |
|
239 |
+ end |
|
183 | 240 |
end |
184 | 241 |
end |
@@ -0,0 +1,81 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Agents::RssAgent do |
|
4 |
+ before do |
|
5 |
+ @valid_options = { |
|
6 |
+ 'expected_update_period_in_days' => "2", |
|
7 |
+ 'url' => "https://github.com/cantino/huginn/commits/master.atom", |
|
8 |
+ } |
|
9 |
+ |
|
10 |
+ stub_request(:any, /github.com/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/github_rss.atom")), :status => 200) |
|
11 |
+ end |
|
12 |
+ |
|
13 |
+ let(:agent) do |
|
14 |
+ _agent = Agents::RssAgent.new(:name => "github rss feed", :options => @valid_options) |
|
15 |
+ _agent.user = users(:bob) |
|
16 |
+ _agent.save! |
|
17 |
+ _agent |
|
18 |
+ end |
|
19 |
+ |
|
20 |
+ it_behaves_like WebRequestConcern |
|
21 |
+ |
|
22 |
+ describe "validations" do |
|
23 |
+ it "should validate the presence of url" do |
|
24 |
+ agent.options['url'] = "http://google.com" |
|
25 |
+ agent.should be_valid |
|
26 |
+ |
|
27 |
+ agent.options['url'] = "" |
|
28 |
+ agent.should_not be_valid |
|
29 |
+ |
|
30 |
+ agent.options['url'] = nil |
|
31 |
+ agent.should_not be_valid |
|
32 |
+ end |
|
33 |
+ |
|
34 |
+ it "should validate the presence and numericality of expected_update_period_in_days" do |
|
35 |
+ agent.options['expected_update_period_in_days'] = "5" |
|
36 |
+ agent.should be_valid |
|
37 |
+ |
|
38 |
+ agent.options['expected_update_period_in_days'] = "wut?" |
|
39 |
+ agent.should_not be_valid |
|
40 |
+ |
|
41 |
+ agent.options['expected_update_period_in_days'] = 0 |
|
42 |
+ agent.should_not be_valid |
|
43 |
+ |
|
44 |
+ agent.options['expected_update_period_in_days'] = nil |
|
45 |
+ agent.should_not be_valid |
|
46 |
+ |
|
47 |
+ agent.options['expected_update_period_in_days'] = "" |
|
48 |
+ agent.should_not be_valid |
|
49 |
+ end |
|
50 |
+ end |
|
51 |
+ |
|
52 |
+ describe "emitting RSS events" do |
|
53 |
+ it "should emit items as events" do |
|
54 |
+ lambda { |
|
55 |
+ agent.check |
|
56 |
+ }.should change { agent.events.count }.by(20) |
|
57 |
+ end |
|
58 |
+ |
|
59 |
+ it "should track ids and not re-emit the same item when seen again" do |
|
60 |
+ agent.check |
|
61 |
+ agent.memory['seen_ids'].should == agent.events.map {|e| e.payload['id'] } |
|
62 |
+ |
|
63 |
+ newest_id = agent.memory['seen_ids'][0] |
|
64 |
+ agent.events.first.payload['id'].should == newest_id |
|
65 |
+ agent.memory['seen_ids'] = agent.memory['seen_ids'][1..-1] # forget the newest id |
|
66 |
+ |
|
67 |
+ lambda { |
|
68 |
+ agent.check |
|
69 |
+ }.should change { agent.events.count }.by(1) |
|
70 |
+ |
|
71 |
+ agent.events.first.payload['id'].should == newest_id |
|
72 |
+ agent.memory['seen_ids'][0].should == newest_id |
|
73 |
+ end |
|
74 |
+ |
|
75 |
+ it "should truncate the seen_ids in memory at 500 items" do |
|
76 |
+ agent.memory['seen_ids'] = ['x'] * 490 |
|
77 |
+ agent.check |
|
78 |
+ agent.memory['seen_ids'].length.should == 500 |
|
79 |
+ end |
|
80 |
+ end |
|
81 |
+end |
@@ -4,23 +4,25 @@ describe Agents::WebsiteAgent do |
||
4 | 4 |
describe "checking without basic auth" do |
5 | 5 |
before do |
6 | 6 |
stub_request(:any, /xkcd/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/xkcd.html")), :status => 200) |
7 |
- @site = { |
|
7 |
+ @valid_options = { |
|
8 | 8 |
'name' => "XKCD", |
9 |
- 'expected_update_period_in_days' => 2, |
|
9 |
+ 'expected_update_period_in_days' => "2", |
|
10 | 10 |
'type' => "html", |
11 | 11 |
'url' => "http://xkcd.com", |
12 | 12 |
'mode' => 'on_change', |
13 | 13 |
'extract' => { |
14 |
- 'url' => { 'css' => "#comic img", 'attr' => "src" }, |
|
15 |
- 'title' => { 'css' => "#comic img", 'attr' => "alt" }, |
|
16 |
- 'hovertext' => { 'css' => "#comic img", 'attr' => "title" } |
|
14 |
+ 'url' => { 'css' => "#comic img", 'value' => "@src" }, |
|
15 |
+ 'title' => { 'css' => "#comic img", 'value' => "@alt" }, |
|
16 |
+ 'hovertext' => { 'css' => "#comic img", 'value' => "@title" } |
|
17 | 17 |
} |
18 | 18 |
} |
19 |
- @checker = Agents::WebsiteAgent.new(:name => "xkcd", :options => @site, :keep_events_for => 2) |
|
19 |
+ @checker = Agents::WebsiteAgent.new(:name => "xkcd", :options => @valid_options, :keep_events_for => 2) |
|
20 | 20 |
@checker.user = users(:bob) |
21 | 21 |
@checker.save! |
22 | 22 |
end |
23 | 23 |
|
24 |
+ it_behaves_like WebRequestConcern |
|
25 |
+ |
|
24 | 26 |
describe "validations" do |
25 | 27 |
before do |
26 | 28 |
@checker.should be_valid |
@@ -42,20 +44,6 @@ describe Agents::WebsiteAgent do |
||
42 | 44 |
@checker.should be_valid |
43 | 45 |
end |
44 | 46 |
|
45 |
- it "should validate headers" do |
|
46 |
- @checker.options['headers'] = "blah" |
|
47 |
- @checker.should_not be_valid |
|
48 |
- |
|
49 |
- @checker.options['headers'] = "" |
|
50 |
- @checker.should be_valid |
|
51 |
- |
|
52 |
- @checker.options['headers'] = {} |
|
53 |
- @checker.should be_valid |
|
54 |
- |
|
55 |
- @checker.options['headers'] = { 'foo' => 'bar' } |
|
56 |
- @checker.should be_valid |
|
57 |
- end |
|
58 |
- |
|
59 | 47 |
it "should validate mode" do |
60 | 48 |
@checker.options['mode'] = "nonsense" |
61 | 49 |
@checker.should_not be_valid |
@@ -97,16 +85,16 @@ describe Agents::WebsiteAgent do |
||
97 | 85 |
|
98 | 86 |
it "should always save events when in :all mode" do |
99 | 87 |
lambda { |
100 |
- @site['mode'] = 'all' |
|
101 |
- @checker.options = @site |
|
88 |
+ @valid_options['mode'] = 'all' |
|
89 |
+ @checker.options = @valid_options |
|
102 | 90 |
@checker.check |
103 | 91 |
@checker.check |
104 | 92 |
}.should change { Event.count }.by(2) |
105 | 93 |
end |
106 | 94 |
|
107 | 95 |
it "should take uniqueness_look_back into account during deduplication" do |
108 |
- @site['mode'] = 'all' |
|
109 |
- @checker.options = @site |
|
96 |
+ @valid_options['mode'] = 'all' |
|
97 |
+ @checker.options = @valid_options |
|
110 | 98 |
@checker.check |
111 | 99 |
@checker.check |
112 | 100 |
event = Event.last |
@@ -114,47 +102,47 @@ describe Agents::WebsiteAgent do |
||
114 | 102 |
event.save |
115 | 103 |
|
116 | 104 |
lambda { |
117 |
- @site['mode'] = 'on_change' |
|
118 |
- @site['uniqueness_look_back'] = 2 |
|
119 |
- @checker.options = @site |
|
105 |
+ @valid_options['mode'] = 'on_change' |
|
106 |
+ @valid_options['uniqueness_look_back'] = 2 |
|
107 |
+ @checker.options = @valid_options |
|
120 | 108 |
@checker.check |
121 | 109 |
}.should_not change { Event.count } |
122 | 110 |
|
123 | 111 |
lambda { |
124 |
- @site['mode'] = 'on_change' |
|
125 |
- @site['uniqueness_look_back'] = 1 |
|
126 |
- @checker.options = @site |
|
112 |
+ @valid_options['mode'] = 'on_change' |
|
113 |
+ @valid_options['uniqueness_look_back'] = 1 |
|
114 |
+ @checker.options = @valid_options |
|
127 | 115 |
@checker.check |
128 | 116 |
}.should change { Event.count }.by(1) |
129 | 117 |
end |
130 | 118 |
|
131 | 119 |
it "should log an error if the number of results for a set of extraction patterns differs" do |
132 |
- @site['extract']['url']['css'] = "div" |
|
133 |
- @checker.options = @site |
|
120 |
+ @valid_options['extract']['url']['css'] = "div" |
|
121 |
+ @checker.options = @valid_options |
|
134 | 122 |
@checker.check |
135 | 123 |
@checker.logs.first.message.should =~ /Got an uneven number of matches/ |
136 | 124 |
end |
137 | 125 |
|
138 | 126 |
it "should accept an array for url" do |
139 |
- @site['url'] = ["http://xkcd.com/1/", "http://xkcd.com/2/"] |
|
140 |
- @checker.options = @site |
|
127 |
+ @valid_options['url'] = ["http://xkcd.com/1/", "http://xkcd.com/2/"] |
|
128 |
+ @checker.options = @valid_options |
|
141 | 129 |
lambda { @checker.save! }.should_not raise_error; |
142 | 130 |
lambda { @checker.check }.should_not raise_error; |
143 | 131 |
end |
144 | 132 |
|
145 | 133 |
it "should parse events from all urls in array" do |
146 | 134 |
lambda { |
147 |
- @site['url'] = ["http://xkcd.com/", "http://xkcd.com/"] |
|
148 |
- @site['mode'] = 'all' |
|
149 |
- @checker.options = @site |
|
135 |
+ @valid_options['url'] = ["http://xkcd.com/", "http://xkcd.com/"] |
|
136 |
+ @valid_options['mode'] = 'all' |
|
137 |
+ @checker.options = @valid_options |
|
150 | 138 |
@checker.check |
151 | 139 |
}.should change { Event.count }.by(2) |
152 | 140 |
end |
153 | 141 |
|
154 | 142 |
it "should follow unique rules when parsing array of urls" do |
155 | 143 |
lambda { |
156 |
- @site['url'] = ["http://xkcd.com/", "http://xkcd.com/"] |
|
157 |
- @checker.options = @site |
|
144 |
+ @valid_options['url'] = ["http://xkcd.com/", "http://xkcd.com/"] |
|
145 |
+ @checker.options = @valid_options |
|
158 | 146 |
@checker.check |
159 | 147 |
}.should change { Event.count }.by(1) |
160 | 148 |
end |
@@ -170,7 +158,7 @@ describe Agents::WebsiteAgent do |
||
170 | 158 |
}, :status => 200) |
171 | 159 |
site = { |
172 | 160 |
'name' => "Some JSON Response", |
173 |
- 'expected_update_period_in_days' => 2, |
|
161 |
+ 'expected_update_period_in_days' => "2", |
|
174 | 162 |
'type' => "json", |
175 | 163 |
'url' => "http://no-encoding.example.com", |
176 | 164 |
'mode' => 'on_change', |
@@ -197,7 +185,7 @@ describe Agents::WebsiteAgent do |
||
197 | 185 |
}, :status => 200) |
198 | 186 |
site = { |
199 | 187 |
'name' => "Some JSON Response", |
200 |
- 'expected_update_period_in_days' => 2, |
|
188 |
+ 'expected_update_period_in_days' => "2", |
|
201 | 189 |
'type' => "json", |
202 | 190 |
'url' => "http://wrong-encoding.example.com", |
203 | 191 |
'mode' => 'on_change', |
@@ -248,11 +236,11 @@ describe Agents::WebsiteAgent do |
||
248 | 236 |
end |
249 | 237 |
|
250 | 238 |
it "parses XPath" do |
251 |
- @site['extract'].each { |key, value| |
|
239 |
+ @valid_options['extract'].each { |key, value| |
|
252 | 240 |
value.delete('css') |
253 | 241 |
value['xpath'] = "//*[@id='comic']//img" |
254 | 242 |
} |
255 |
- @checker.options = @site |
|
243 |
+ @checker.options = @valid_options |
|
256 | 244 |
@checker.check |
257 | 245 |
event = Event.last |
258 | 246 |
event.payload['url'].should == "http://imgs.xkcd.com/comics/evolving.png" |
@@ -263,13 +251,12 @@ describe Agents::WebsiteAgent do |
||
263 | 251 |
it "should turn relative urls to absolute" do |
264 | 252 |
rel_site = { |
265 | 253 |
'name' => "XKCD", |
266 |
- 'expected_update_period_in_days' => 2, |
|
254 |
+ 'expected_update_period_in_days' => "2", |
|
267 | 255 |
'type' => "html", |
268 | 256 |
'url' => "http://xkcd.com", |
269 | 257 |
'mode' => "on_change", |
270 | 258 |
'extract' => { |
271 |
- 'url' => {'css' => "#topLeft a", 'attr' => "href"}, |
|
272 |
- 'title' => {'css' => "#topLeft a", 'text' => "true"} |
|
259 |
+ 'url' => {'css' => "#topLeft a", 'value' => "@href"}, |
|
273 | 260 |
} |
274 | 261 |
} |
275 | 262 |
rel = Agents::WebsiteAgent.new(:name => "xkcd", :options => rel_site) |
@@ -280,6 +267,44 @@ describe Agents::WebsiteAgent do |
||
280 | 267 |
event.payload['url'].should == "http://xkcd.com/about" |
281 | 268 |
end |
282 | 269 |
|
270 |
+ it "should return an integer value if XPath evaluates to one" do |
|
271 |
+ rel_site = { |
|
272 |
+ 'name' => "XKCD", |
|
273 |
+ 'expected_update_period_in_days' => 2, |
|
274 |
+ 'type' => "html", |
|
275 |
+ 'url' => "http://xkcd.com", |
|
276 |
+ 'mode' => "on_change", |
|
277 |
+ 'extract' => { |
|
278 |
+ 'num_links' => {'css' => "#comicLinks", 'value' => "count(./a)"} |
|
279 |
+ } |
|
280 |
+ } |
|
281 |
+ rel = Agents::WebsiteAgent.new(:name => "xkcd", :options => rel_site) |
|
282 |
+ rel.user = users(:bob) |
|
283 |
+ rel.save! |
|
284 |
+ rel.check |
|
285 |
+ event = Event.last |
|
286 |
+ event.payload['num_links'].should == "9" |
|
287 |
+ end |
|
288 |
+ |
|
289 |
+ it "should return all texts concatenated if XPath returns many text nodes" do |
|
290 |
+ rel_site = { |
|
291 |
+ 'name' => "XKCD", |
|
292 |
+ 'expected_update_period_in_days' => 2, |
|
293 |
+ 'type' => "html", |
|
294 |
+ 'url' => "http://xkcd.com", |
|
295 |
+ 'mode' => "on_change", |
|
296 |
+ 'extract' => { |
|
297 |
+ 'slogan' => {'css' => "#slogan", 'value' => ".//text()"} |
|
298 |
+ } |
|
299 |
+ } |
|
300 |
+ rel = Agents::WebsiteAgent.new(:name => "xkcd", :options => rel_site) |
|
301 |
+ rel.user = users(:bob) |
|
302 |
+ rel.save! |
|
303 |
+ rel.check |
|
304 |
+ event = Event.last |
|
305 |
+ event.payload['slogan'].should == "A webcomic of romance, sarcasm, math, and language." |
|
306 |
+ end |
|
307 |
+ |
|
283 | 308 |
describe "JSON" do |
284 | 309 |
it "works with paths" do |
285 | 310 |
json = { |
@@ -291,7 +316,7 @@ describe Agents::WebsiteAgent do |
||
291 | 316 |
stub_request(:any, /json-site/).to_return(:body => json.to_json, :status => 200) |
292 | 317 |
site = { |
293 | 318 |
'name' => "Some JSON Response", |
294 |
- 'expected_update_period_in_days' => 2, |
|
319 |
+ 'expected_update_period_in_days' => "2", |
|
295 | 320 |
'type' => "json", |
296 | 321 |
'url' => "http://json-site.com", |
297 | 322 |
'mode' => 'on_change', |
@@ -322,7 +347,7 @@ describe Agents::WebsiteAgent do |
||
322 | 347 |
stub_request(:any, /json-site/).to_return(:body => json.to_json, :status => 200) |
323 | 348 |
site = { |
324 | 349 |
'name' => "Some JSON Response", |
325 |
- 'expected_update_period_in_days' => 2, |
|
350 |
+ 'expected_update_period_in_days' => "2", |
|
326 | 351 |
'type' => "json", |
327 | 352 |
'url' => "http://json-site.com", |
328 | 353 |
'mode' => 'on_change', |
@@ -358,7 +383,7 @@ describe Agents::WebsiteAgent do |
||
358 | 383 |
stub_request(:any, /json-site/).to_return(:body => json.to_json, :status => 200) |
359 | 384 |
site = { |
360 | 385 |
'name' => "Some JSON Response", |
361 |
- 'expected_update_period_in_days' => 2, |
|
386 |
+ 'expected_update_period_in_days' => "2", |
|
362 | 387 |
'type' => "json", |
363 | 388 |
'url' => "http://json-site.com", |
364 | 389 |
'mode' => 'on_change' |
@@ -382,7 +407,7 @@ describe Agents::WebsiteAgent do |
||
382 | 407 |
@event.payload = { 'url' => "http://xkcd.com" } |
383 | 408 |
|
384 | 409 |
lambda { |
385 |
- @checker.options = @site |
|
410 |
+ @checker.options = @valid_options |
|
386 | 411 |
@checker.receive([@event]) |
387 | 412 |
}.should change { Event.count }.by(1) |
388 | 413 |
end |
@@ -394,20 +419,20 @@ describe Agents::WebsiteAgent do |
||
394 | 419 |
stub_request(:any, /example/). |
395 | 420 |
with(headers: { 'Authorization' => "Basic #{['user:pass'].pack('m').chomp}" }). |
396 | 421 |
to_return(:body => File.read(Rails.root.join("spec/data_fixtures/xkcd.html")), :status => 200) |
397 |
- @site = { |
|
422 |
+ @valid_options = { |
|
398 | 423 |
'name' => "XKCD", |
399 |
- 'expected_update_period_in_days' => 2, |
|
424 |
+ 'expected_update_period_in_days' => "2", |
|
400 | 425 |
'type' => "html", |
401 | 426 |
'url' => "http://www.example.com", |
402 | 427 |
'mode' => 'on_change', |
403 | 428 |
'extract' => { |
404 |
- 'url' => { 'css' => "#comic img", 'attr' => "src" }, |
|
405 |
- 'title' => { 'css' => "#comic img", 'attr' => "alt" }, |
|
406 |
- 'hovertext' => { 'css' => "#comic img", 'attr' => "title" } |
|
429 |
+ 'url' => { 'css' => "#comic img", 'value' => "@src" }, |
|
430 |
+ 'title' => { 'css' => "#comic img", 'value' => "@alt" }, |
|
431 |
+ 'hovertext' => { 'css' => "#comic img", 'value' => "@title" } |
|
407 | 432 |
}, |
408 | 433 |
'basic_auth' => "user:pass" |
409 | 434 |
} |
410 |
- @checker = Agents::WebsiteAgent.new(:name => "auth", :options => @site) |
|
435 |
+ @checker = Agents::WebsiteAgent.new(:name => "auth", :options => @valid_options) |
|
411 | 436 |
@checker.user = users(:bob) |
412 | 437 |
@checker.save! |
413 | 438 |
end |
@@ -425,18 +450,18 @@ describe Agents::WebsiteAgent do |
||
425 | 450 |
stub_request(:any, /example/). |
426 | 451 |
with(headers: { 'foo' => 'bar', 'user_agent' => /Faraday/ }). |
427 | 452 |
to_return(:body => File.read(Rails.root.join("spec/data_fixtures/xkcd.html")), :status => 200) |
428 |
- @site = { |
|
453 |
+ @valid_options = { |
|
429 | 454 |
'name' => "XKCD", |
430 |
- 'expected_update_period_in_days' => 2, |
|
455 |
+ 'expected_update_period_in_days' => "2", |
|
431 | 456 |
'type' => "html", |
432 | 457 |
'url' => "http://www.example.com", |
433 | 458 |
'mode' => 'on_change', |
434 | 459 |
'headers' => { 'foo' => 'bar' }, |
435 | 460 |
'extract' => { |
436 |
- 'url' => { 'css' => "#comic img", 'attr' => "src" }, |
|
461 |
+ 'url' => { 'css' => "#comic img", 'value' => "@src" }, |
|
437 | 462 |
} |
438 | 463 |
} |
439 |
- @checker = Agents::WebsiteAgent.new(:name => "ua", :options => @site) |
|
464 |
+ @checker = Agents::WebsiteAgent.new(:name => "ua", :options => @valid_options) |
|
440 | 465 |
@checker.user = users(:bob) |
441 | 466 |
@checker.save! |
442 | 467 |
end |
@@ -76,3 +76,39 @@ describe Event do |
||
76 | 76 |
end |
77 | 77 |
end |
78 | 78 |
end |
79 |
+ |
|
80 |
+describe EventDrop do |
|
81 |
+ def interpolate(string, event) |
|
82 |
+ event.agent.interpolate_string(string, event.to_liquid) |
|
83 |
+ end |
|
84 |
+ |
|
85 |
+ before do |
|
86 |
+ @event = Event.new |
|
87 |
+ @event.agent = agents(:jane_weather_agent) |
|
88 |
+ @event.payload = { |
|
89 |
+ 'title' => 'some title', |
|
90 |
+ 'url' => 'http://some.site.example.org/', |
|
91 |
+ } |
|
92 |
+ @event.save! |
|
93 |
+ end |
|
94 |
+ |
|
95 |
+ it 'should be created via Agent#to_liquid' do |
|
96 |
+ @event.to_liquid.class.should be(EventDrop) |
|
97 |
+ end |
|
98 |
+ |
|
99 |
+ it 'should have attributes of its payload' do |
|
100 |
+ t = '{{title}}: {{url}}' |
|
101 |
+ interpolate(t, @event).should eq('some title: http://some.site.example.org/') |
|
102 |
+ end |
|
103 |
+ |
|
104 |
+ it 'should be iteratable' do |
|
105 |
+ # to_liquid returns self |
|
106 |
+ t = "{% for pair in to_liquid %}{{pair | join:':' }}\n{% endfor %}" |
|
107 |
+ interpolate(t, @event).should eq("title:some title\nurl:http://some.site.example.org/\n") |
|
108 |
+ end |
|
109 |
+ |
|
110 |
+ it 'should have agent' do |
|
111 |
+ t = '{{agent.name}}' |
|
112 |
+ interpolate(t, @event).should eq('SF Weather') |
|
113 |
+ end |
|
114 |
+end |
@@ -0,0 +1,88 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+shared_examples_for EmailConcern do |
|
4 |
+ let(:valid_options) { |
|
5 |
+ { |
|
6 |
+ :subject => "hello!", |
|
7 |
+ :expected_receive_period_in_days => "2" |
|
8 |
+ } |
|
9 |
+ } |
|
10 |
+ |
|
11 |
+ let(:agent) do |
|
12 |
+ _agent = described_class.new(:name => "some email agent", :options => valid_options) |
|
13 |
+ _agent.user = users(:jane) |
|
14 |
+ _agent |
|
15 |
+ end |
|
16 |
+ |
|
17 |
+ describe "validations" do |
|
18 |
+ it "should be valid" do |
|
19 |
+ agent.should be_valid |
|
20 |
+ end |
|
21 |
+ |
|
22 |
+ it "should validate the presence of 'subject'" do |
|
23 |
+ agent.options['subject'] = '' |
|
24 |
+ agent.should_not be_valid |
|
25 |
+ |
|
26 |
+ agent.options['subject'] = nil |
|
27 |
+ agent.should_not be_valid |
|
28 |
+ end |
|
29 |
+ |
|
30 |
+ it "should validate the presence of 'expected_receive_period_in_days'" do |
|
31 |
+ agent.options['expected_receive_period_in_days'] = '' |
|
32 |
+ agent.should_not be_valid |
|
33 |
+ |
|
34 |
+ agent.options['expected_receive_period_in_days'] = nil |
|
35 |
+ agent.should_not be_valid |
|
36 |
+ end |
|
37 |
+ |
|
38 |
+ it "should validate that recipients, when provided, is one or more valid email addresses" do |
|
39 |
+ agent.options['recipients'] = '' |
|
40 |
+ agent.should be_valid |
|
41 |
+ |
|
42 |
+ agent.options['recipients'] = nil |
|
43 |
+ agent.should be_valid |
|
44 |
+ |
|
45 |
+ agent.options['recipients'] = 'bob@example.com' |
|
46 |
+ agent.should be_valid |
|
47 |
+ |
|
48 |
+ agent.options['recipients'] = ['bob@example.com'] |
|
49 |
+ agent.should be_valid |
|
50 |
+ |
|
51 |
+ agent.options['recipients'] = ['bob@example.com', 'jane@example.com'] |
|
52 |
+ agent.should be_valid |
|
53 |
+ |
|
54 |
+ agent.options['recipients'] = ['bob@example.com', 'example.com'] |
|
55 |
+ agent.should_not be_valid |
|
56 |
+ |
|
57 |
+ agent.options['recipients'] = ['hi!'] |
|
58 |
+ agent.should_not be_valid |
|
59 |
+ |
|
60 |
+ agent.options['recipients'] = { :foo => "bar" } |
|
61 |
+ agent.should_not be_valid |
|
62 |
+ |
|
63 |
+ agent.options['recipients'] = "wut" |
|
64 |
+ agent.should_not be_valid |
|
65 |
+ end |
|
66 |
+ end |
|
67 |
+ |
|
68 |
+ describe "#recipients" do |
|
69 |
+ it "defaults to the user's email address" do |
|
70 |
+ agent.recipients.should == [users(:jane).email] |
|
71 |
+ end |
|
72 |
+ |
|
73 |
+ it "wraps a string with an array" do |
|
74 |
+ agent.options['recipients'] = 'bob@bob.com' |
|
75 |
+ agent.recipients.should == ['bob@bob.com'] |
|
76 |
+ end |
|
77 |
+ |
|
78 |
+ it "handles an array" do |
|
79 |
+ agent.options['recipients'] = ['bob@bob.com', 'jane@jane.com'] |
|
80 |
+ agent.recipients.should == ['bob@bob.com', 'jane@jane.com'] |
|
81 |
+ end |
|
82 |
+ |
|
83 |
+ it "interpolates" do |
|
84 |
+ agent.options['recipients'] = "{{ username }}@{{ domain }}" |
|
85 |
+ agent.recipients('username' => 'bob', 'domain' => 'example.com').should == ["bob@example.com"] |
|
86 |
+ end |
|
87 |
+ end |
|
88 |
+end |
@@ -20,7 +20,7 @@ shared_examples_for LiquidInterpolatable do |
||
20 | 20 |
|
21 | 21 |
describe "interpolating liquid templates" do |
22 | 22 |
it "should work" do |
23 |
- @checker.interpolate_options(@checker.options, @event.payload).should == { |
|
23 |
+ @checker.interpolate_options(@checker.options, @event).should == { |
|
24 | 24 |
"normal" => "just some normal text", |
25 | 25 |
"variable" => "hello", |
26 | 26 |
"text" => "Some test with an embedded hello", |
@@ -30,7 +30,7 @@ shared_examples_for LiquidInterpolatable do |
||
30 | 30 |
|
31 | 31 |
it "should work with arrays", focus: true do |
32 | 32 |
@checker.options = {"value" => ["{{variable}}", "Much array", "Hey, {{hello_world}}"]} |
33 |
- @checker.interpolate_options(@checker.options, @event.payload).should == { |
|
33 |
+ @checker.interpolate_options(@checker.options, @event).should == { |
|
34 | 34 |
"value" => ["hello", "Much array", "Hey, Hello world"] |
35 | 35 |
} |
36 | 36 |
end |
@@ -38,7 +38,7 @@ shared_examples_for LiquidInterpolatable do |
||
38 | 38 |
it "should work recursively" do |
39 | 39 |
@checker.options['hash'] = {'recursive' => "{{variable}}"} |
40 | 40 |
@checker.options['indifferent_hash'] = ActiveSupport::HashWithIndifferentAccess.new({'recursive' => "{{variable}}"}) |
41 |
- @checker.interpolate_options(@checker.options, @event.payload).should == { |
|
41 |
+ @checker.interpolate_options(@checker.options, @event).should == { |
|
42 | 42 |
"normal" => "just some normal text", |
43 | 43 |
"variable" => "hello", |
44 | 44 |
"text" => "Some test with an embedded hello", |
@@ -49,8 +49,8 @@ shared_examples_for LiquidInterpolatable do |
||
49 | 49 |
end |
50 | 50 |
|
51 | 51 |
it "should work for strings" do |
52 |
- @checker.interpolate_string("{{variable}}", @event.payload).should == "hello" |
|
53 |
- @checker.interpolate_string("{{variable}} you", @event.payload).should == "hello you" |
|
52 |
+ @checker.interpolate_string("{{variable}}", @event).should == "hello" |
|
53 |
+ @checker.interpolate_string("{{variable}} you", @event).should == "hello you" |
|
54 | 54 |
end |
55 | 55 |
end |
56 | 56 |
|
@@ -0,0 +1,66 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+shared_examples_for WebRequestConcern do |
|
4 |
+ let(:agent) do |
|
5 |
+ _agent = described_class.new(:name => "some agent", :options => @valid_options || {}) |
|
6 |
+ _agent.user = users(:jane) |
|
7 |
+ _agent |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ describe "validations" do |
|
11 |
+ it "should be valid" do |
|
12 |
+ agent.should be_valid |
|
13 |
+ end |
|
14 |
+ |
|
15 |
+ it "should validate user_agent" do |
|
16 |
+ agent.options['user_agent'] = nil |
|
17 |
+ agent.should be_valid |
|
18 |
+ |
|
19 |
+ agent.options['user_agent'] = "" |
|
20 |
+ agent.should be_valid |
|
21 |
+ |
|
22 |
+ agent.options['user_agent'] = "foo" |
|
23 |
+ agent.should be_valid |
|
24 |
+ |
|
25 |
+ agent.options['user_agent'] = ["foo"] |
|
26 |
+ agent.should_not be_valid |
|
27 |
+ |
|
28 |
+ agent.options['user_agent'] = 1 |
|
29 |
+ agent.should_not be_valid |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ it "should validate headers" do |
|
33 |
+ agent.options['headers'] = "blah" |
|
34 |
+ agent.should_not be_valid |
|
35 |
+ |
|
36 |
+ agent.options['headers'] = "" |
|
37 |
+ agent.should be_valid |
|
38 |
+ |
|
39 |
+ agent.options['headers'] = {} |
|
40 |
+ agent.should be_valid |
|
41 |
+ |
|
42 |
+ agent.options['headers'] = { 'foo' => 'bar' } |
|
43 |
+ agent.should be_valid |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ it "should validate basic_auth" do |
|
47 |
+ agent.options['basic_auth'] = "foo:bar" |
|
48 |
+ agent.should be_valid |
|
49 |
+ |
|
50 |
+ agent.options['basic_auth'] = ["foo", "bar"] |
|
51 |
+ agent.should be_valid |
|
52 |
+ |
|
53 |
+ agent.options['basic_auth'] = "" |
|
54 |
+ agent.should be_valid |
|
55 |
+ |
|
56 |
+ agent.options['basic_auth'] = nil |
|
57 |
+ agent.should be_valid |
|
58 |
+ |
|
59 |
+ agent.options['basic_auth'] = "blah" |
|
60 |
+ agent.should_not be_valid |
|
61 |
+ |
|
62 |
+ agent.options['basic_auth'] = ["blah"] |
|
63 |
+ agent.should_not be_valid |
|
64 |
+ end |
|
65 |
+ end |
|
66 |
+end |
@@ -0,0 +1,9 @@ |
||
1 |
+require 'vcr' |
|
2 |
+ |
|
3 |
+VCR.configure do |c| |
|
4 |
+ c.cassette_library_dir = 'spec/cassettes' |
|
5 |
+ c.allow_http_connections_when_no_cassette = true |
|
6 |
+ c.hook_into :webmock |
|
7 |
+ c.default_cassette_options = { record: :new_episodes} |
|
8 |
+ c.configure_rspec_metadata! |
|
9 |
+end |